Skip to content
Merged
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
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
wasm_size_limit_bytes:
description: Maximum optimized contract WASM size in bytes
required: false
default: "65536"

jobs:
contract-build:
Expand All @@ -25,6 +31,15 @@ jobs:
run: cargo build --target wasm32v1-none --release
working-directory: contracts

- uses: actions/setup-node@v6
with:
node-version: 20

- name: Check optimized WASM size
run: node scripts/check-wasm-size.mjs --file contracts/target/wasm32v1-none/release/token_factory.wasm --max-bytes "$WASM_SIZE_LIMIT_BYTES"
env:
WASM_SIZE_LIMIT_BYTES: ${{ github.event.inputs.wasm_size_limit_bytes || '65536' }}

contract-test:
name: Contract Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -131,4 +146,3 @@ jobs:
- name: Stop Local Stellar Network
if: always()
run: docker-compose -f docker-compose.e2e.yml down

78 changes: 78 additions & 0 deletions scripts/check-wasm-size.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node
import { stat } from "node:fs/promises";
import { fileURLToPath } from "node:url";

export const DEFAULT_WASM_SIZE_LIMIT_BYTES = 64 * 1024;

const formatter = new Intl.NumberFormat("en-US");

const formatBytes = (bytes) => `${formatter.format(bytes)} bytes`;

export const parseByteLimit = (value) => {
if (value === undefined || value === "") return DEFAULT_WASM_SIZE_LIMIT_BYTES;

const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(
`WASM size limit must be a positive integer byte count, got "${value}"`,
);
}

return parsed;
};

export const checkWasmSize = async ({
file,
limitBytes = DEFAULT_WASM_SIZE_LIMIT_BYTES,
}) => {
if (!file) {
throw new Error("Missing required --file path to optimized contract WASM");
}

const { size } = await stat(file);
if (size > limitBytes) {
throw new Error(
`${file} exceeds Soroban WASM size limit: ${formatBytes(size)} > ${formatBytes(limitBytes)}`,
);
}

return { file, sizeBytes: size, limitBytes };
};

const parseArgs = (argv) => {
const args = { file: undefined, limitBytes: undefined };

for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--file") {
args.file = argv[index + 1];
index += 1;
continue;
}
if (arg === "--max-bytes" || arg === "--limit-bytes") {
args.limitBytes = parseByteLimit(argv[index + 1]);
index += 1;
continue;
}
throw new Error(`Unknown argument "${arg}"`);
}

return args;
};

const main = async () => {
const { file, limitBytes } = parseArgs(process.argv.slice(2));
const result = await checkWasmSize({ file, limitBytes });

console.log(
`WASM size OK: ${result.file} is ${formatBytes(result.sizeBytes)} ` +
`(limit ${formatBytes(result.limitBytes)})`,
);
};

if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
54 changes: 54 additions & 0 deletions scripts/check-wasm-size.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
checkWasmSize,
DEFAULT_WASM_SIZE_LIMIT_BYTES,
parseByteLimit,
} from "./check-wasm-size.mjs";

const withFixture = async (bytes, testFn) => {
const dir = await mkdtemp(join(tmpdir(), "stellar-forge-wasm-size-"));
try {
const file = join(dir, "token_factory.wasm");
await writeFile(file, Buffer.alloc(bytes));
await testFn(file);
} finally {
await rm(dir, { force: true, recursive: true });
}
};

describe("check-wasm-size", () => {
it("uses a 64 KiB default limit", () => {
assert.equal(DEFAULT_WASM_SIZE_LIMIT_BYTES, 64 * 1024);
});

it("parses a positive byte limit", () => {
assert.equal(parseByteLimit("98304"), 98304);
});

it("rejects invalid byte limits", () => {
assert.throws(() => parseByteLimit("0"), /positive integer/);
assert.throws(() => parseByteLimit("64kb"), /positive integer/);
});

it("passes when the optimized WASM is at or below the limit", async () => {
await withFixture(64 * 1024, async (file) => {
const result = await checkWasmSize({ file, limitBytes: 64 * 1024 });

assert.equal(result.sizeBytes, 64 * 1024);
assert.equal(result.limitBytes, 64 * 1024);
});
});

it("fails with a descriptive message when the optimized WASM exceeds the limit", async () => {
await withFixture(64 * 1024 + 1, async (file) => {
await assert.rejects(
() => checkWasmSize({ file, limitBytes: 64 * 1024 }),
/exceeds Soroban WASM size limit: 65,537 bytes > 65,536 bytes/,
);
});
});
});
Loading