From d5441fa981607595819c62821716a041cde50cd6 Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Wed, 22 Apr 2026 15:37:40 +0200 Subject: [PATCH 01/15] wasm test + ci pipeline --- .github/workflows/ci.yml | 69 ++++++++++++++++++++ scripts/test_wasm.mjs | 137 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/test_wasm.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6f64699 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: extractions/setup-just@v2 + + - run: go test ./... + + - run: just build-linux-local + + - run: just build-wasm + + - name: Install wasm_exec.js + run: | + SRC="$(go env GOROOT)/lib/wasm/wasm_exec.js" + [ -f "$SRC" ] || SRC="$(go env GOROOT)/misc/wasm/wasm_exec.js" + cp "$SRC" ./build/wasm_exec.js + + - run: node ./scripts/test_wasm.mjs + + - uses: actions/upload-artifact@v4 + with: + name: lighter-go-linux-wasm + path: | + build/lighter-signer-linux.so + build/lighter-signer-linux.h + build/lighter-signer.wasm + build/wasm_exec.js + + build-darwin: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: extractions/setup-just@v2 + + - run: just build-darwin-local + + - uses: actions/upload-artifact@v4 + with: + name: lighter-go-darwin + path: build/lighter-signer-darwin-arm64.dylib \ No newline at end of file diff --git a/scripts/test_wasm.mjs b/scripts/test_wasm.mjs new file mode 100644 index 0000000..62b8d33 --- /dev/null +++ b/scripts/test_wasm.mjs @@ -0,0 +1,137 @@ +import assert from "node:assert/strict"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, ".."); +const buildDir = path.join(repoRoot, "build"); + +await import(path.join(buildDir, "wasm_exec.js")); + +const go = new globalThis.Go(); +const wasmBytes = fs.readFileSync(path.join(buildDir, "lighter-signer.wasm")); +const { instance } = await WebAssembly.instantiate(wasmBytes, go.importObject); + +go.run(instance); + +function assertHex(name, value) { + assert.equal(typeof value, "string", `${name} should be a string`); + assert.match(value, /^0x[0-9a-f]+$/i, `${name} should be a hex string`); +} + +function assertNoError(name, result) { + assert.ok(result && typeof result === "object", `${name} should return an object`); + assert.equal(result.error, undefined, `${name} failed: ${result.error}`); +} + +function assertSignedTx(name, result, expectedTxType) { + assertNoError(name, result); + assert.equal(result.txType, expectedTxType, `${name} returned an unexpected txType`); + assert.equal(typeof result.txInfo, "string", `${name} should return txInfo`); + assert.equal(typeof result.txHash, "string", `${name} should return txHash`); + assert.match(result.txHash, /^[0-9a-f]+$/i, `${name} txHash should be hex`); + + const txInfo = JSON.parse(result.txInfo); + assert.equal(typeof txInfo.Nonce, "number", `${name} txInfo should include Nonce`); + assert.equal(typeof txInfo.Sig, "string", `${name} txInfo should include Sig`); + return txInfo; +} + +function assertSkipNonceAttr(name, txInfo, expected) { + if (expected) { + assert.equal(txInfo.L2TxAttributes?.["4"], 1, `${name} should include skipNonce attribute`); + return; + } + assert.equal(txInfo.L2TxAttributes, null, `${name} should not include tx attributes`); +} + +const keyResult = globalThis.GenerateAPIKey(); +console.log("GenerateAPIKey:", keyResult); +assertNoError("GenerateAPIKey", keyResult); +assertHex("GenerateAPIKey.privateKey", keyResult.privateKey); +assertHex("GenerateAPIKey.publicKey", keyResult.publicKey); +const privateKey = keyResult.privateKey; + +const createResult = globalThis.CreateClient("http://localhost:1234", privateKey, 304, 0, 1); +console.log("CreateClient:", createResult); +assertNoError("CreateClient", createResult); + +const cancelResult = globalThis.SignCancelOrder(0, 12345, 1, 42, 0, 1); +console.log("SignCancelOrder (skipNonce=1):", cancelResult); +const cancelTxInfo = assertSignedTx("SignCancelOrder (skipNonce=1)", cancelResult, 15); +assert.equal(cancelTxInfo.AccountIndex, 1); +assert.equal(cancelTxInfo.ApiKeyIndex, 0); +assert.equal(cancelTxInfo.MarketIndex, 0); +assert.equal(cancelTxInfo.Index, 12345); +assert.equal(cancelTxInfo.Nonce, 42); +assertSkipNonceAttr("SignCancelOrder (skipNonce=1)", cancelTxInfo, true); + +const cancelResult2 = globalThis.SignCancelOrder(0, 12345, 0, 42, 0, 1); +console.log("SignCancelOrder (skipNonce=0):", cancelResult2); +const cancelTxInfo2 = assertSignedTx("SignCancelOrder (skipNonce=0)", cancelResult2, 15); +assert.equal(cancelTxInfo2.Nonce, 42); +assertSkipNonceAttr("SignCancelOrder (skipNonce=0)", cancelTxInfo2, false); +assert.notEqual(cancelResult.txHash, cancelResult2.txHash, "skipNonce should affect the signed tx hash"); + +const cancelAllResult = globalThis.SignCancelAllOrders(0, 0, 1, 42, 0, 1); +console.log("SignCancelAllOrders (skipNonce=1):", cancelAllResult); +const cancelAllTxInfo = assertSignedTx("SignCancelAllOrders (skipNonce=1)", cancelAllResult, 16); +assert.equal(cancelAllTxInfo.TimeInForce, 0); +assert.equal(cancelAllTxInfo.Time, 0); +assertSkipNonceAttr("SignCancelAllOrders (skipNonce=1)", cancelAllTxInfo, true); + +const orderResult = globalThis.SignCreateOrder( + 0, 1, 1000, 50000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 42, 0, 1 +); +console.log("SignCreateOrder (skipNonce=1):", orderResult); +const orderTxInfo = assertSignedTx("SignCreateOrder (skipNonce=1)", orderResult, 14); +assert.equal(orderTxInfo.MarketIndex, 0); +assert.equal(orderTxInfo.ClientOrderIndex, 1); +assert.equal(orderTxInfo.BaseAmount, 1000); +assert.equal(orderTxInfo.Price, 50000); +assert.equal(orderTxInfo.TimeInForce, 0); +assert.equal(orderTxInfo.OrderExpiry, 0); +assertSkipNonceAttr("SignCreateOrder (skipNonce=1)", orderTxInfo, true); + +const subAccResult = globalThis.SignCreateSubAccount(1, 42, 0, 1); +console.log("SignCreateSubAccount (skipNonce=1):", subAccResult); +const subAccTxInfo = assertSignedTx("SignCreateSubAccount (skipNonce=1)", subAccResult, 9); +assert.equal(subAccTxInfo.AccountIndex, 1); +assert.equal(subAccTxInfo.Nonce, 42); +assertSkipNonceAttr("SignCreateSubAccount (skipNonce=1)", subAccTxInfo, true); + +const levResult = globalThis.SignUpdateLeverage(0, 100, 0, 1, 42, 0, 1); +console.log("SignUpdateLeverage (skipNonce=1):", levResult); +const levTxInfo = assertSignedTx("SignUpdateLeverage (skipNonce=1)", levResult, 20); +assert.equal(levTxInfo.MarketIndex, 0); +assert.equal(levTxInfo.InitialMarginFraction, 100); +assert.equal(levTxInfo.MarginMode, 0); +assertSkipNonceAttr("SignUpdateLeverage (skipNonce=1)", levTxInfo, true); + +const expiry = Date.now() + 7 * 24 * 60 * 60 * 1000; + +const groupedResult = globalThis.SignCreateGroupedOrders( + 1, + [ + { + MarketIndex: 0, ClientOrderIndex: 0, BaseAmount: 1000, Price: 50000, + IsAsk: 0, Type: 0, TimeInForce: 1, ReduceOnly: 0, TriggerPrice: 0, OrderExpiry: expiry, + }, + { + MarketIndex: 0, ClientOrderIndex: 0, BaseAmount: 0, Price: 51000, + IsAsk: 1, Type: 4, TimeInForce: 0, ReduceOnly: 1, TriggerPrice: 49000, OrderExpiry: expiry, + }, + ], + 0, 0, 0, 1, 42, 0, 1 +); +console.log("SignCreateGroupedOrders (skipNonce=1):", groupedResult); +const groupedTxInfo = assertSignedTx("SignCreateGroupedOrders (skipNonce=1)", groupedResult, 28); +assert.equal(groupedTxInfo.GroupingType, 1); +assert.equal(groupedTxInfo.Orders.length, 2); +assert.equal(groupedTxInfo.Orders[0].TimeInForce, 1); +assert.equal(groupedTxInfo.Orders[1].Type, 4); +assert.equal(groupedTxInfo.Orders[1].TriggerPrice, 49000); +assertSkipNonceAttr("SignCreateGroupedOrders (skipNonce=1)", groupedTxInfo, true); + +console.log("\n--- All assertions passed ---"); From 2941879074809d7210586391236f0605a59185de Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Wed, 22 Apr 2026 15:41:46 +0200 Subject: [PATCH 02/15] make it on all push --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f64699..206f63c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,6 @@ name: CI on: push: - branches: [main] pull_request: workflow_dispatch: From f007271410d816d15fc01abb9c52180a6b6ce0cf Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Wed, 22 Apr 2026 15:45:52 +0200 Subject: [PATCH 03/15] fix: go version for wasm --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 206f63c..c246d31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version: '1.23.1' cache: true - uses: actions/setup-node@v4 From 4ffcb4ec435f16181c55217776319a6da1622abc Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Wed, 22 Apr 2026 16:48:11 +0200 Subject: [PATCH 04/15] add tests for go --- client/sign_test.go | 282 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 client/sign_test.go diff --git a/client/sign_test.go b/client/sign_test.go new file mode 100644 index 0000000..f206129 --- /dev/null +++ b/client/sign_test.go @@ -0,0 +1,282 @@ +package client + +import ( + "testing" + + "github.com/elliottech/lighter-go/types" + "github.com/elliottech/lighter-go/types/txtypes" +) + +const ( + testChainID uint32 = 304 + testAccountIndex int64 = 1 + testAPIKeyIndex uint8 = 0 + testNonce int64 = 42 +) + +func newTestClient(t *testing.T, privateKey string) *TxClient { + t.Helper() + c, err := NewTxClient(nil, privateKey, testAccountIndex, testAPIKeyIndex, testChainID) + if err != nil { + t.Fatalf("NewTxClient failed: %v", err) + } + return c +} + +func opsWithSkipNonce(skipNonce uint8, nonce int64) *types.TransactOpts { + attr := &types.L2TxAttributes{} + if skipNonce == 1 { + sn := skipNonce + attr.SkipNonce = &sn + } + n := nonce + return &types.TransactOpts{ + Nonce: &n, + TxAttributes: attr, + } +} + +func assertSkipNonce(t *testing.T, name string, attrs txtypes.L2TxAttributes, expectedSet bool) { + t.Helper() + if expectedSet { + if attrs == nil { + t.Errorf("%s: attributes map should be non-nil when skipNonce=1", name) + return + } + att, ok := attrs[txtypes.AttributeTypeSkipTxNonce] + if !ok { + t.Errorf("%s: SkipTxNonce key missing", name) + return + } + if att != 1 { + t.Errorf("%s: SkipTxNonce = %d, want 1", name, att) + } + return + } + if attrs != nil { + t.Errorf("%s: attributes map should be nil when skipNonce=0, got %v", name, attrs) + } +} + +func TestGenerateAPIKey(t *testing.T) { + priv, pub, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + if len(priv) < 3 || priv[:2] != "0x" { + t.Errorf("privateKey should be a 0x-prefixed hex string, got %q", priv) + } + if len(pub) < 3 || pub[:2] != "0x" { + t.Errorf("publicKey should be a 0x-prefixed hex string, got %q", pub) + } +} + +func TestCreateClient(t *testing.T) { + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + c := newTestClient(t, priv) + if c.GetChainId() != testChainID { + t.Errorf("chainId = %d, want %d", c.GetChainId(), testChainID) + } + if c.GetAccountIndex() != testAccountIndex { + t.Errorf("accountIndex = %d, want %d", c.GetAccountIndex(), testAccountIndex) + } + if c.GetApiKeyIndex() != testAPIKeyIndex { + t.Errorf("apiKeyIndex = %d, want %d", c.GetApiKeyIndex(), testAPIKeyIndex) + } +} + +func TestSignCancelOrder(t *testing.T) { + priv, _, _ := GenerateAPIKey() + c := newTestClient(t, priv) + req := &types.CancelOrderTxReq{MarketIndex: 0, Index: 12345} + + // skipNonce = 1 + tx1, err := c.GetCancelOrderTransaction(req, opsWithSkipNonce(1, testNonce)) + if err != nil { + t.Fatalf("GetCancelOrderTransaction (skipNonce=1) failed: %v", err) + } + if tx1.GetTxType() != txtypes.TxTypeL2CancelOrder { + t.Errorf("txType = %d, want %d", tx1.GetTxType(), txtypes.TxTypeL2CancelOrder) + } + if tx1.AccountIndex != testAccountIndex { + t.Errorf("AccountIndex = %d, want %d", tx1.AccountIndex, testAccountIndex) + } + if tx1.ApiKeyIndex != testAPIKeyIndex { + t.Errorf("ApiKeyIndex = %d, want %d", tx1.ApiKeyIndex, testAPIKeyIndex) + } + if tx1.MarketIndex != 0 { + t.Errorf("MarketIndex = %d, want 0", tx1.MarketIndex) + } + if tx1.Index != 12345 { + t.Errorf("Index = %d, want 12345", tx1.Index) + } + if tx1.Nonce != testNonce { + t.Errorf("Nonce = %d, want %d", tx1.Nonce, testNonce) + } + if len(tx1.Sig) == 0 { + t.Error("Sig should not be empty") + } + assertSkipNonce(t, "SignCancelOrder(skipNonce=1)", tx1.L2TxAttributes, true) + + tx2, err := c.GetCancelOrderTransaction(req, opsWithSkipNonce(0, testNonce)) + if err != nil { + t.Fatalf("GetCancelOrderTransaction (skipNonce=0) failed: %v", err) + } + assertSkipNonce(t, "SignCancelOrder(skipNonce=0)", tx2.L2TxAttributes, false) + + if tx1.SignedHash == tx2.SignedHash { + t.Error("skipNonce flag should affect the signed tx hash, but hashes are identical") + } +} + +func TestSignCancelAllOrders(t *testing.T) { + priv, _, _ := GenerateAPIKey() + c := newTestClient(t, priv) + req := &types.CancelAllOrdersTxReq{TimeInForce: 0, Time: 0} + + tx, err := c.GetCancelAllOrdersTransaction(req, opsWithSkipNonce(1, testNonce)) + if err != nil { + t.Fatalf("GetCancelAllOrdersTransaction failed: %v", err) + } + if tx.GetTxType() != txtypes.TxTypeL2CancelAllOrders { + t.Errorf("txType = %d, want %d", tx.GetTxType(), txtypes.TxTypeL2CancelAllOrders) + } + if tx.TimeInForce != 0 { + t.Errorf("TimeInForce = %d, want 0", tx.TimeInForce) + } + if tx.Time != 0 { + t.Errorf("Time = %d, want 0", tx.Time) + } + assertSkipNonce(t, "SignCancelAllOrders(skipNonce=1)", tx.L2TxAttributes, true) +} + +func TestSignCreateOrder(t *testing.T) { + priv, _, _ := GenerateAPIKey() + c := newTestClient(t, priv) + + req := &types.CreateOrderTxReq{ + MarketIndex: 0, + ClientOrderIndex: 1, + BaseAmount: 1000, + Price: 50000, + IsAsk: 0, + Type: 0, + TimeInForce: 0, + ReduceOnly: 0, + TriggerPrice: 0, + OrderExpiry: 0, + } + tx, err := c.GetCreateOrderTransaction(req, opsWithSkipNonce(1, testNonce)) + if err != nil { + t.Fatalf("GetCreateOrderTransaction failed: %v", err) + } + if tx.GetTxType() != txtypes.TxTypeL2CreateOrder { + t.Errorf("txType = %d, want %d", tx.GetTxType(), txtypes.TxTypeL2CreateOrder) + } + if tx.MarketIndex != 0 { + t.Errorf("MarketIndex = %d, want 0", tx.MarketIndex) + } + if tx.ClientOrderIndex != 1 { + t.Errorf("ClientOrderIndex = %d, want 1", tx.ClientOrderIndex) + } + if tx.BaseAmount != 1000 { + t.Errorf("BaseAmount = %d, want 1000", tx.BaseAmount) + } + if tx.Price != 50000 { + t.Errorf("Price = %d, want 50000", tx.Price) + } + assertSkipNonce(t, "SignCreateOrder(skipNonce=1)", tx.L2TxAttributes, true) +} + +func TestSignCreateSubAccount(t *testing.T) { + priv, _, _ := GenerateAPIKey() + c := newTestClient(t, priv) + + tx, err := c.GetCreateSubAccountTransaction(opsWithSkipNonce(1, testNonce)) + if err != nil { + t.Fatalf("GetCreateSubAccountTransaction failed: %v", err) + } + if tx.GetTxType() != txtypes.TxTypeL2CreateSubAccount { + t.Errorf("txType = %d, want %d", tx.GetTxType(), txtypes.TxTypeL2CreateSubAccount) + } + if tx.AccountIndex != testAccountIndex { + t.Errorf("AccountIndex = %d, want %d", tx.AccountIndex, testAccountIndex) + } + if tx.Nonce != testNonce { + t.Errorf("Nonce = %d, want %d", tx.Nonce, testNonce) + } + assertSkipNonce(t, "SignCreateSubAccount(skipNonce=1)", tx.L2TxAttributes, true) +} + +func TestSignUpdateLeverage(t *testing.T) { + priv, _, _ := GenerateAPIKey() + c := newTestClient(t, priv) + + req := &types.UpdateLeverageTxReq{ + MarketIndex: 0, + InitialMarginFraction: 100, + MarginMode: 0, + } + tx, err := c.GetUpdateLeverageTransaction(req, opsWithSkipNonce(1, testNonce)) + if err != nil { + t.Fatalf("GetUpdateLeverageTransaction failed: %v", err) + } + if tx.GetTxType() != txtypes.TxTypeL2UpdateLeverage { + t.Errorf("txType = %d, want %d", tx.GetTxType(), txtypes.TxTypeL2UpdateLeverage) + } + if tx.MarketIndex != 0 { + t.Errorf("MarketIndex = %d, want 0", tx.MarketIndex) + } + if tx.InitialMarginFraction != 100 { + t.Errorf("InitialMarginFraction = %d, want 100", tx.InitialMarginFraction) + } + assertSkipNonce(t, "SignUpdateLeverage(skipNonce=1)", tx.L2TxAttributes, true) +} + +func TestSignCreateGroupedOrders(t *testing.T) { + priv, _, _ := GenerateAPIKey() + c := newTestClient(t, priv) + + const expiry int64 = 7 * 24 * 60 * 60 * 1000 + + req := &types.CreateGroupedOrdersTxReq{ + GroupingType: 1, + Orders: []*types.CreateOrderTxReq{ + { + MarketIndex: 0, ClientOrderIndex: 0, BaseAmount: 1000, Price: 50000, + IsAsk: 0, Type: 0, TimeInForce: 1, ReduceOnly: 0, TriggerPrice: 0, OrderExpiry: expiry, + }, + { + MarketIndex: 0, ClientOrderIndex: 0, BaseAmount: 0, Price: 51000, + IsAsk: 1, Type: 4, TimeInForce: 0, ReduceOnly: 1, TriggerPrice: 49000, OrderExpiry: expiry, + }, + }, + } + + tx, err := c.GetCreateGroupedOrdersTransaction(req, opsWithSkipNonce(1, testNonce)) + if err != nil { + t.Fatalf("GetCreateGroupedOrdersTransaction failed: %v", err) + } + if tx.GetTxType() != txtypes.TxTypeL2CreateGroupedOrders { + t.Errorf("txType = %d, want %d", tx.GetTxType(), txtypes.TxTypeL2CreateGroupedOrders) + } + if tx.GroupingType != 1 { + t.Errorf("GroupingType = %d, want 1", tx.GroupingType) + } + if len(tx.Orders) != 2 { + t.Fatalf("Orders len = %d, want 2", len(tx.Orders)) + } + if tx.Orders[0].TimeInForce != 1 { + t.Errorf("Orders[0].TimeInForce = %d, want 1", tx.Orders[0].TimeInForce) + } + if tx.Orders[1].Type != 4 { + t.Errorf("Orders[1].Type = %d, want 4", tx.Orders[1].Type) + } + if tx.Orders[1].TriggerPrice != 49000 { + t.Errorf("Orders[1].TriggerPrice = %d, want 49000", tx.Orders[1].TriggerPrice) + } + assertSkipNonce(t, "SignCreateGroupedOrders(skipNonce=1)", tx.L2TxAttributes, true) +} From 4b75b9ac54d15b0c2f64b4edb69d66f3f991cb4d Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Thu, 23 Apr 2026 12:47:57 +0200 Subject: [PATCH 05/15] Added java,rust examples, modified ci pipeline --- .github/workflows/ci.yml | 22 +- .gitignore | 14 +- examples/README.md | 169 ++++- examples/{ => cpp}/example.cpp | 16 +- examples/java/pom.xml | 36 + examples/java/src/main/java/Example.java | 120 ++++ examples/java/src/main/java/LighterLib.java | 206 ++++++ examples/rust/Cargo.lock | 32 + examples/rust/Cargo.toml | 15 + examples/rust/src/lib.rs | 706 ++++++++++++++++++++ examples/rust/src/main.rs | 129 ++++ {scripts => examples/wasm}/test_wasm.mjs | 2 +- justfile | 8 +- 13 files changed, 1464 insertions(+), 11 deletions(-) rename examples/{ => cpp}/example.cpp (73%) create mode 100644 examples/java/pom.xml create mode 100644 examples/java/src/main/java/Example.java create mode 100644 examples/java/src/main/java/LighterLib.java create mode 100644 examples/rust/Cargo.lock create mode 100644 examples/rust/Cargo.toml create mode 100644 examples/rust/src/lib.rs create mode 100644 examples/rust/src/main.rs rename {scripts => examples/wasm}/test_wasm.mjs (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c246d31..784e733 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,18 @@ jobs: with: node-version: 22 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: examples/rust + - uses: extractions/setup-just@v2 - run: go test ./... @@ -37,7 +49,12 @@ jobs: [ -f "$SRC" ] || SRC="$(go env GOROOT)/misc/wasm/wasm_exec.js" cp "$SRC" ./build/wasm_exec.js - - run: node ./scripts/test_wasm.mjs + - name: Run WASM example + run: node ./examples/wasm/test_wasm.mjs + + - run: just build-java + + - run: just build-rust - uses: actions/upload-artifact@v4 with: @@ -62,6 +79,9 @@ jobs: - run: just build-darwin-local + - name: Compile C++ example + run: clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-darwin-arm64.dylib -o ./build/example-cpp + - uses: actions/upload-artifact@v4 with: name: lighter-go-darwin diff --git a/.gitignore b/.gitignore index 54fa9d4..1c8d418 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,16 @@ vendor build/* !build/.keep -/build \ No newline at end of file +/build + + +# Shared library build outputs +sharedlib/lighter.dylib +sharedlib/lighter.so +sharedlib/lighter.h + +# Maven +examples/java/target/ + +# Rust +examples/rust/target/ \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 9a2eb74..9758744 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,171 @@ -## C++ +# C++ Compile the example, using the following command (select the correct shared library) ``` -clang++ -std=c++20 -O3 ./examples/example.cpp ./build/lighter-signer-darwin-arm64.dylib -o ./build/example-cpp +clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-darwin-arm64.dylib -o ./build/example-cpp ``` -Run the example from the `./build` folder as `./example-cpp` \ No newline at end of file +Run the example from the `./build` folder as `./example-cpp` + +# Java + +JNA bindings for the lighter-go shared library, with a benchmark. + +### Prerequisites + +- Java 21+ +- Maven +- Go (to build the shared library) + +Install on macOS: +``` +brew install --cask temurin +brew install maven +``` + +### Build + +**1. Build the shared library** from the repo root: + +``` +go build -buildmode=c-shared -o sharedlib/lighter.dylib ./sharedlib # macOS +go build -buildmode=c-shared -o sharedlib/lighter.so ./sharedlib # Linux +``` + +**2. Compile** from the `examples/java/` directory: + +``` +cd examples/java +mvn compile +``` + +### Run + +``` +mvn exec:java +``` + +### What the benchmark does + +Spawns 5 threads, each of which: +1. Generates a fresh API key pair +2. Creates a client on chain 304 +3. Obtains an auth token (7-hour expiry) +4. Signs 100 create-order + cancel-order pairs back to back +5. Prints elapsed time for the signing loop + + +# Rust + +FFI bindings for the lighter-go shared library, with a benchmark. + +### Prerequisites + +- Rust (stable, 1.70+) +- Go (to build the shared library) + +Install Rust via [rustup](https://rustup.rs/): +``` +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +### Build + +**1. Build the shared library** from the repo root: + +``` +go build -buildmode=c-shared -o sharedlib/lighter.dylib ./sharedlib # macOS +go build -buildmode=c-shared -o sharedlib/lighter.so ./sharedlib # Linux +``` + +**2. Compile** from the `examples/rust/` directory: + +``` +cd examples/rust +cargo build --release +``` + +### Run + +``` +cargo run --release +``` + +### What the benchmark does + +Spawns 5 threads, each of which: +1. Generates a fresh API key pair +2. Creates a client on chain 304 +3. Obtains an auth token (7-hour expiry) +4. Signs 100 create-order + cancel-order pairs back to back +5. Prints elapsed time for the signing loop + +### Sample output + +``` +[3] publicKey=0xdb20326defefbe7671156f61d927c2c6... +[1] publicKey=0x152911bbbcb8353c98502905fa8e6339... +[4] publicKey=0x51c684fa2248dfc8e58c74ec9d3414a5... +[2] publicKey=0xbf262215de6727d495b3f6f07b224229... +[0] publicKey=0xf0fea739719c12d1e62235b8a546c313... +[2] authToken=1776311597710:100:2:60d78eb8... +[0] authToken=1776311597710:100:0:da74b9d3... +[1] authToken=1776311597710:100:1:40463366... +[3] authToken=1776311597710:100:3:4051768060... +[4] authToken=1776311597710:100:4:4e37a696... +[1] 100 create+cancel pairs in 47.19 ms +[0] 100 create+cancel pairs in 47.22 ms +[4] 100 create+cancel pairs in 47.20 ms +[2] 100 create+cancel pairs in 47.43 ms +[3] 100 create+cancel pairs in 47.55 ms +``` + +# WASM + +Node smoke test that loads the lighter-go WASM build and exercises the signer globals. + +### Prerequisites + +- Node.js 22+ +- Go 1.23+ (to build the WASM artifact) +- [just](https://github.com/casey/just) (optional, for the build recipe) + +Install on macOS: +``` +brew install node go just +``` + +### Build + +**1. Build the WASM artifact** from the repo root: + +``` +just build-wasm +# or, equivalently: +GOOS=js GOARCH=wasm go build -trimpath -o ./build/lighter-signer.wasm ./wasm/ +``` + +**2. Install `wasm_exec.js`** into `./build` (the script loads it from there): + +``` +SRC="$(go env GOROOT)/lib/wasm/wasm_exec.js" +[ -f "$SRC" ] || SRC="$(go env GOROOT)/misc/wasm/wasm_exec.js" +cp "$SRC" ./build/wasm_exec.js +``` + +### Run + +From the repo root: + +``` +node ./examples/wasm/test_wasm.mjs +``` + +### What the script does + +1. Instantiates `build/lighter-signer.wasm` via `wasm_exec.js` and starts the Go runtime +2. Calls `GenerateAPIKey()` and asserts a valid hex keypair is returned +3. Calls `CreateClient(...)` on chain 304 with the generated private key +4. Signs a cancel-order, cancel-all-orders, create-order, create-sub-account and update-leverage transaction +5. For each signed tx, asserts the `txType`, `txHash` and decoded `txInfo` fields match the inputs +6. Verifies that toggling the `skipNonce` flag changes the resulting tx hash and populates the `L2TxAttributes` accordingly \ No newline at end of file diff --git a/examples/example.cpp b/examples/cpp/example.cpp similarity index 73% rename from examples/example.cpp rename to examples/cpp/example.cpp index 116704d..d5d5f45 100644 --- a/examples/example.cpp +++ b/examples/cpp/example.cpp @@ -3,7 +3,7 @@ #include #include #include -#include "../build/lighter-signer-darwin-arm64.h" +#include "../../build/lighter-signer-darwin-arm64.h" using namespace std; @@ -24,7 +24,7 @@ uint64_t now_ms() { void run_example(int apiKeyIndex) { // Example: generate an API key - ApiKeyResponse apiResp = GenerateAPIKey(nullptr); + ApiKeyResponse apiResp = GenerateAPIKey(); if (apiResp.err != nullptr) { return; @@ -46,7 +46,15 @@ void run_example(int apiKeyIndex) { for (int i = 1; i <= 100; i += 1) { // create an order to sell 1 ETH @ 4000 w/ a deadline 60 mins in the future // limit post only order - auto create = SignCreateOrder(0, i, 10000, 400000, true, /* cOrderType */ 0, /* cTimeInForce */ 2, /* cReduceOnly */ 0, /* cTriggerPrice */ 0, now_ms() + 60 * 60 * 1000, nonce, apiKeyIndex, accountIndex); + auto create = SignCreateOrder( + 0, i, 10000, 400000, true, + /* cOrderType */ 0, /* cTimeInForce */ 2, /* cReduceOnly */ 0, /* cTriggerPrice */ 0, + now_ms() + 60 * 60 * 1000, + /* cIntegratorAccountIndex */ 0, + /* cIntegratorTakerFee */ 0, + /* cIntegratorMakerFee */ 0, + /* cSkipNonce */ 0, + nonce, apiKeyIndex, accountIndex); nonce += 1; if (create.err != nullptr) { @@ -54,7 +62,7 @@ void run_example(int apiKeyIndex) { } // cancel order with client order id i on ETH market (market ID 0) - auto cancel = SignCancelOrder(0, i, nonce, apiKeyIndex, accountIndex); + auto cancel = SignCancelOrder(0, i, /* cSkipNonce */ 0, nonce, apiKeyIndex, accountIndex); nonce += 1; if (cancel.err != nullptr) { diff --git a/examples/java/pom.xml b/examples/java/pom.xml new file mode 100644 index 0000000..a2ce73e --- /dev/null +++ b/examples/java/pom.xml @@ -0,0 +1,36 @@ + + 4.0.0 + + com.elliottech.lighter + lighter-java + 1.0-SNAPSHOT + + + 21 + + + + + net.java.dev.jna + jna + 5.14.0 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.3.0 + + Example + --enable-native-access=ALL-UNNAMED + + + + + \ No newline at end of file diff --git a/examples/java/src/main/java/Example.java b/examples/java/src/main/java/Example.java new file mode 100644 index 0000000..990aefa --- /dev/null +++ b/examples/java/src/main/java/Example.java @@ -0,0 +1,120 @@ +import java.util.ArrayList; +import java.util.List; + +/** + * Mirrors examples/example.cpp — generates API keys, creates a client, + * gets an auth token, then runs 5 threads each signing 100 create+cancel pairs. + * + * Build & run: + * javac -cp jna-5.14.0.jar:. LighterLib.java Example.java + * java --enable-native-access=ALL-UNNAMED -cp jna-5.14.0.jar:. Example + */ +public class Example { + + static final int CHAIN_ID = 304; + static final long ACCOUNT_INDEX = 100L; + static final int MARKET_INDEX = 0; // ETH market + static final long BASE_AMOUNT = 10_000L; + static final int PRICE = 400_000; + static final int ORDER_TYPE = 0; // limit + static final int TIME_IN_FORCE = 2; // post-only + static final int N_THREADS = 5; + static final int N_ORDERS = 100; + + static long nowMs() { return System.currentTimeMillis(); } + static long nowUs() { return System.nanoTime() / 1_000; } + + static void runExample(LighterLib.Lib lib, int apiKeyIndex) { + // Generate a fresh API key pair + LighterLib.ApiKeyResponse.ByValue apiResp = lib.GenerateAPIKey(); + if (apiResp.err != null) { + System.err.println("[" + apiKeyIndex + "] GenerateAPIKey error: " + apiResp.err); + return; + } + System.out.println("[" + apiKeyIndex + "] publicKey=" + apiResp.publicKey); + + // Create a client bound to the generated key + String err = lib.CreateClient(null, apiResp.privateKey, CHAIN_ID, apiKeyIndex, ACCOUNT_INDEX); + if (err != null) { + System.err.println("[" + apiKeyIndex + "] CreateClient error: " + err); + return; + } + + // Auth token valid for 7 hours + long tokenDeadline = nowMs() + 7L * 60 * 60 * 1000; + LighterLib.StrOrErr.ByValue tokenResp = lib.CreateAuthToken(tokenDeadline, apiKeyIndex, ACCOUNT_INDEX); + if (tokenResp.err != null) { + System.err.println("[" + apiKeyIndex + "] CreateAuthToken error: " + tokenResp.err); + return; + } + System.out.println("[" + apiKeyIndex + "] authToken=" + tokenResp.str); + + long nonce = 1L; + long start = nowUs(); + + for (int i = 1; i <= N_ORDERS; i++) { + long orderExpiry = nowMs() + 60L * 60 * 1000; // 60 min from now + + // Sign a limit post-only ask order + LighterLib.SignedTxResponse.ByValue create = lib.SignCreateOrder( + MARKET_INDEX, + (long) i, // clientOrderIndex + BASE_AMOUNT, + PRICE, + /* isAsk */ 1, + ORDER_TYPE, + TIME_IN_FORCE, + /* reduceOnly */ 0, + /* triggerPrice */ 0, + orderExpiry, + /* integratorAccountIndex */ 0L, + /* integratorTakerFee */ 0, + /* integratorMakerFee */ 0, + /* skipNonce */ (byte) 0, + nonce, + apiKeyIndex, + ACCOUNT_INDEX + ); + nonce++; + + if (create.err != null) { + System.err.println("[" + apiKeyIndex + "] SignCreateOrder(" + i + ") error: " + create.err); + } + + // Cancel the same order by client order index + LighterLib.SignedTxResponse.ByValue cancel = lib.SignCancelOrder( + MARKET_INDEX, + (long) i, + /* skipNonce */ (byte) 0, + nonce, + apiKeyIndex, + ACCOUNT_INDEX + ); + nonce++; + + if (cancel.err != null) { + System.err.println("[" + apiKeyIndex + "] SignCancelOrder(" + i + ") error: " + cancel.err); + } + } + + long elapsed = nowUs() - start; + System.out.printf("[%d] %d create+cancel pairs in %.2f ms%n", + apiKeyIndex, N_ORDERS, elapsed / 1000.0); + } + + public static void main(String[] args) throws InterruptedException { + LighterLib.Lib lib = LighterLib.loadFromDir("../../sharedlib"); + + List threads = new ArrayList<>(); + for (int i = 0; i < N_THREADS; i++) { + final int apiKeyIndex = i; + Thread t = new Thread(() -> runExample(lib, apiKeyIndex)); + t.start(); + threads.add(t); + } + + for (Thread t : threads) { + t.join(); + } + } +} \ No newline at end of file diff --git a/examples/java/src/main/java/LighterLib.java b/examples/java/src/main/java/LighterLib.java new file mode 100644 index 0000000..c5cab2e --- /dev/null +++ b/examples/java/src/main/java/LighterLib.java @@ -0,0 +1,206 @@ +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.Structure.FieldOrder; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * JNA bindings for the lighter-go shared library. + * + * Build the .dylib first: + * go build -buildmode=c-shared -o lighter.dylib . (macOS) + * go build -buildmode=c-shared -o lighter.so . (Linux) + * + * Load: + * Lib lib = LighterLib.load("/abs/path/to/lighter.dylib"); + * Lib lib = LighterLib.loadFromDir("../sharedlib"); + */ +public class LighterLib { + + // ------------------------------------------------------------------------- + // Structs + // ------------------------------------------------------------------------- + + @FieldOrder({"str", "err"}) + public static class StrOrErr extends Structure { + public String str; + public String err; + + public static class ByValue extends StrOrErr implements Structure.ByValue {} + + public String unwrap() { + if (err != null) throw new RuntimeException(err); + return str; + } + } + + @FieldOrder({"privateKey", "publicKey", "err"}) + public static class ApiKeyResponse extends Structure { + public String privateKey; + public String publicKey; + public String err; + + public static class ByValue extends ApiKeyResponse implements Structure.ByValue {} + + public void check() { + if (err != null) throw new RuntimeException(err); + } + } + + // uint8_t txType sits at offset 0; the next field is a pointer which requires + // 8-byte alignment on 64-bit platforms, so 7 bytes of padding follow txType. + @FieldOrder({"txType", "_pad", "txInfo", "txHash", "messageToSign", "err"}) + public static class SignedTxResponse extends Structure { + public byte txType; + public byte[] _pad = new byte[7]; + public String txInfo; + public String txHash; + public String messageToSign; + public String err; + + public static class ByValue extends SignedTxResponse implements Structure.ByValue {} + + public void check() { + if (err != null) throw new RuntimeException(err); + } + } + + @FieldOrder({"MarketIndex", "ClientOrderIndex", "BaseAmount", "Price", + "IsAsk", "Type", "TimeInForce", "ReduceOnly", "TriggerPrice", "OrderExpiry"}) + public static class CreateOrderTxReq extends Structure { + public short MarketIndex; + public long ClientOrderIndex; + public long BaseAmount; + public int Price; + public byte IsAsk; + public byte Type; + public byte TimeInForce; + public byte ReduceOnly; + public int TriggerPrice; + public long OrderExpiry; + + public static CreateOrderTxReq[] allocateArray(int size) { + return (CreateOrderTxReq[]) new CreateOrderTxReq().toArray(size); + } + } + + // ------------------------------------------------------------------------- + // JNA interface — all struct return values are ByValue + // ------------------------------------------------------------------------- + + public interface Lib extends Library { + ApiKeyResponse.ByValue GenerateAPIKey(); + + String CreateClient(String url, String privateKey, int chainId, + int apiKeyIndex, long accountIndex); + + String CheckClient(int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignChangePubKey(String pubKey, byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignCreateOrder( + int marketIndex, long clientOrderIndex, long baseAmount, + int price, int isAsk, int orderType, int timeInForce, + int reduceOnly, int triggerPrice, long orderExpiry, + long integratorAccountIndex, int integratorTakerFee, int integratorMakerFee, + byte skipNonce, long nonce, int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignCreateGroupedOrders( + byte groupingType, CreateOrderTxReq orders, int len, + long integratorAccountIndex, int integratorTakerFee, int integratorMakerFee, + byte skipNonce, long nonce, int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignCancelOrder(int marketIndex, long orderIndex, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignWithdraw(int assetIndex, int routeType, long amount, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignCreateSubAccount(byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignCancelAllOrders(int timeInForce, long time, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignModifyOrder( + int marketIndex, long index, long baseAmount, long price, long triggerPrice, + long integratorAccountIndex, int integratorTakerFee, int integratorMakerFee, + byte skipNonce, long nonce, int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignTransfer( + long toAccountIndex, short assetIndex, byte fromRouteType, byte toRouteType, + long amount, long usdcFee, String memo, + byte skipNonce, long nonce, int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignCreatePublicPool(long operatorFee, int initialTotalShares, + long minOperatorShareRate, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignUpdatePublicPool(long publicPoolIndex, int status, + long operatorFee, int minOperatorShareRate, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignMintShares(long publicPoolIndex, long shareAmount, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignBurnShares(long publicPoolIndex, long shareAmount, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignUpdateLeverage(int marketIndex, int initialMarginFraction, + int marginMode, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + StrOrErr.ByValue CreateAuthToken(long deadline, int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignUpdateMargin(int marketIndex, long usdcAmount, int direction, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignStakeAssets(long stakingPoolIndex, long shareAmount, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignUnstakeAssets(long stakingPoolIndex, long shareAmount, + byte skipNonce, long nonce, + int apiKeyIndex, long accountIndex); + + SignedTxResponse.ByValue SignApproveIntegrator( + long integratorIndex, + int maxPerpsTakerFee, int maxPerpsMakerFee, + int maxSpotTakerFee, int maxSpotMakerFee, + long approvalExpiry, + byte skipNonce, long nonce, int apiKeyIndex, long accountIndex); + + void Free(Pointer ptr); + } + + // ------------------------------------------------------------------------- + // Loader helpers + // ------------------------------------------------------------------------- + + public static Lib load(String absolutePath) { + return Native.load(absolutePath, Lib.class); + } + + public static Lib loadFromDir(String relativeDir) { + String ext = System.getProperty("os.name").toLowerCase().contains("mac") ? "dylib" : "so"; + Path lib = Paths.get(System.getProperty("user.dir")) + .resolve(relativeDir) + .resolve("lighter." + ext) + .toAbsolutePath(); + return load(lib.toString()); + } +} \ No newline at end of file diff --git a/examples/rust/Cargo.lock b/examples/rust/Cargo.lock new file mode 100644 index 0000000..ddab07b --- /dev/null +++ b/examples/rust/Cargo.lock @@ -0,0 +1,32 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "lighter-rust" +version = "0.1.0" +dependencies = [ + "libloading", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml new file mode 100644 index 0000000..c67ce6f --- /dev/null +++ b/examples/rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lighter-rust" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "example" +path = "src/main.rs" + +[lib] +name = "lighter_rust" +path = "src/lib.rs" + +[dependencies] +libloading = "0.8" \ No newline at end of file diff --git a/examples/rust/src/lib.rs b/examples/rust/src/lib.rs new file mode 100644 index 0000000..4d921bb --- /dev/null +++ b/examples/rust/src/lib.rs @@ -0,0 +1,706 @@ +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +use libloading::{Library, Symbol}; + +// ------------------------------------------------------------------------- +// Raw C structs — repr(C) inserts the same padding the C/Go ABI uses +// ------------------------------------------------------------------------- + +/// Mirrors `StrOrErr` from lighter.h +#[repr(C)] +pub struct RawStrOrErr { + pub str_: *mut c_char, + pub err: *mut c_char, +} + +/// Mirrors `ApiKeyResponse` from lighter.h +#[repr(C)] +pub struct RawApiKeyResponse { + pub private_key: *mut c_char, + pub public_key: *mut c_char, + pub err: *mut c_char, +} + +/// Mirrors `SignedTxResponse` from lighter.h. +/// +/// txType (1 byte) is followed by 7 bytes of implicit padding on 64-bit +/// before the first pointer — repr(C) handles this automatically. +#[repr(C)] +pub struct RawSignedTxResponse { + pub tx_type: u8, + pub tx_info: *mut c_char, + pub tx_hash: *mut c_char, + pub message_to_sign: *mut c_char, + pub err: *mut c_char, +} + +/// Mirrors `CreateOrderTxReq` from lighter.h +#[repr(C)] +pub struct CreateOrderTxReq { + pub market_index: i16, + pub client_order_index: i64, + pub base_amount: i64, + pub price: u32, + pub is_ask: u8, + pub r#type: u8, + pub time_in_force: u8, + pub reduce_only: u8, + pub trigger_price: u32, + pub order_expiry: i64, +} + +// ------------------------------------------------------------------------- +// Idiomatic Rust wrappers +// ------------------------------------------------------------------------- + +#[derive(Debug)] +pub struct StrOrErr { + pub value: Option, + pub err: Option, +} + +impl StrOrErr { + pub fn unwrap_value(self) -> Result { + match self.err { + Some(e) => Err(e), + None => Ok(self.value.unwrap_or_default()), + } + } +} + +#[derive(Debug)] +pub struct ApiKeyResponse { + pub private_key: Option, + pub public_key: Option, + pub err: Option, +} + +impl ApiKeyResponse { + pub fn check(self) -> Result<(String, String), String> { + match self.err { + Some(e) => Err(e), + None => Ok(( + self.private_key.unwrap_or_default(), + self.public_key.unwrap_or_default(), + )), + } + } +} + +#[derive(Debug)] +pub struct SignedTxResponse { + pub tx_type: u8, + pub tx_info: Option, + pub tx_hash: Option, + pub message_to_sign: Option, + pub err: Option, +} + +impl SignedTxResponse { + pub fn check(self) -> Result { + match self.err { + Some(e) => Err(e), + None => Ok(self), + } + } +} + +// ------------------------------------------------------------------------- +// Internal helpers +// ------------------------------------------------------------------------- + +unsafe fn ptr_to_string(ptr: *mut c_char) -> Option { + if ptr.is_null() { + None + } else { + Some(CStr::from_ptr(ptr).to_string_lossy().into_owned()) + } +} + +fn raw_to_signed_tx(raw: RawSignedTxResponse) -> SignedTxResponse { + unsafe { + SignedTxResponse { + tx_type: raw.tx_type, + tx_info: ptr_to_string(raw.tx_info), + tx_hash: ptr_to_string(raw.tx_hash), + message_to_sign: ptr_to_string(raw.message_to_sign), + err: ptr_to_string(raw.err), + } + } +} + +// ------------------------------------------------------------------------- +// LighterLib — loads the shared library dynamically (mirrors Java JNA usage) +// ------------------------------------------------------------------------- + +pub struct LighterLib { + lib: Library, +} + +// The Go shared library uses its own goroutine scheduler and internal locking; +// all exported C functions are safe to call from multiple threads concurrently. +unsafe impl Send for LighterLib {} +unsafe impl Sync for LighterLib {} + +impl LighterLib { + /// Load by absolute path. + pub fn load(path: &str) -> Result { + let lib = unsafe { Library::new(path)? }; + Ok(Self { lib }) + } + + /// Load `lighter.{dylib,so}` from a directory relative to the working directory. + pub fn load_from_dir(dir: &str) -> Result { + let ext = if cfg!(target_os = "macos") { "dylib" } else { "so" }; + let mut path = std::env::current_dir().expect("current dir"); + path.push(dir); + path.push(format!("lighter.{}", ext)); + Self::load(path.to_string_lossy().as_ref()) + } + + // ------------------------------------------------------------------------- + // Functions + // ------------------------------------------------------------------------- + + pub fn generate_api_key(&self) -> ApiKeyResponse { + unsafe { + let f: Symbol RawApiKeyResponse> = + self.lib.get(b"GenerateAPIKey\0").unwrap(); + let raw = f(); + ApiKeyResponse { + private_key: ptr_to_string(raw.private_key), + public_key: ptr_to_string(raw.public_key), + err: ptr_to_string(raw.err), + } + } + } + + /// Returns `None` on success, `Some(err_message)` on failure. + pub fn create_client( + &self, + url: Option<&str>, + private_key: &str, + chain_id: i32, + api_key_index: i32, + account_index: i64, + ) -> Option { + let url_c = url.map(|u| CString::new(u).unwrap()); + let pk_c = CString::new(private_key).unwrap(); + unsafe { + let f: Symbol< + unsafe extern "C" fn(*mut c_char, *mut c_char, i32, i32, i64) -> *mut c_char, + > = self.lib.get(b"CreateClient\0").unwrap(); + let url_ptr = url_c + .as_ref() + .map_or(std::ptr::null_mut(), |c| c.as_ptr() as *mut c_char); + ptr_to_string(f( + url_ptr, + pk_c.as_ptr() as *mut c_char, + chain_id, + api_key_index, + account_index, + )) + } + } + + pub fn check_client(&self, api_key_index: i32, account_index: i64) -> Option { + unsafe { + let f: Symbol *mut c_char> = + self.lib.get(b"CheckClient\0").unwrap(); + ptr_to_string(f(api_key_index, account_index)) + } + } + + pub fn sign_change_pub_key( + &self, + pub_key: &str, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + let pk_c = CString::new(pub_key).unwrap(); + unsafe { + let f: Symbol< + unsafe extern "C" fn(*mut c_char, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignChangePubKey\0").unwrap(); + raw_to_signed_tx(f( + pk_c.as_ptr() as *mut c_char, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_create_order( + &self, + market_index: i32, + client_order_index: i64, + base_amount: i64, + price: i32, + is_ask: i32, + order_type: i32, + time_in_force: i32, + reduce_only: i32, + trigger_price: i32, + order_expiry: i64, + integrator_account_index: i64, + integrator_taker_fee: i32, + integrator_maker_fee: i32, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn( + i32, i64, i64, i32, i32, i32, i32, i32, i32, i64, + i64, i32, i32, u8, i64, i32, i64, + ) -> RawSignedTxResponse, + > = self.lib.get(b"SignCreateOrder\0").unwrap(); + raw_to_signed_tx(f( + market_index, + client_order_index, + base_amount, + price, + is_ask, + order_type, + time_in_force, + reduce_only, + trigger_price, + order_expiry, + integrator_account_index, + integrator_taker_fee, + integrator_maker_fee, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_create_grouped_orders( + &self, + grouping_type: u8, + orders: &[CreateOrderTxReq], + integrator_account_index: i64, + integrator_taker_fee: i32, + integrator_maker_fee: i32, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn( + u8, *const CreateOrderTxReq, i32, + i64, i32, i32, u8, i64, i32, i64, + ) -> RawSignedTxResponse, + > = self.lib.get(b"SignCreateGroupedOrders\0").unwrap(); + raw_to_signed_tx(f( + grouping_type, + orders.as_ptr(), + orders.len() as i32, + integrator_account_index, + integrator_taker_fee, + integrator_maker_fee, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn sign_cancel_order( + &self, + market_index: i32, + order_index: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i32, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignCancelOrder\0").unwrap(); + raw_to_signed_tx(f( + market_index, + order_index, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn sign_withdraw( + &self, + asset_index: i32, + route_type: i32, + amount: u64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i32, i32, u64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignWithdraw\0").unwrap(); + raw_to_signed_tx(f( + asset_index, route_type, amount, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + pub fn sign_create_sub_account( + &self, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignCreateSubAccount\0").unwrap(); + raw_to_signed_tx(f(skip_nonce, nonce, api_key_index, account_index)) + } + } + + pub fn sign_cancel_all_orders( + &self, + time_in_force: i32, + time: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i32, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignCancelAllOrders\0").unwrap(); + raw_to_signed_tx(f( + time_in_force, time, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_modify_order( + &self, + market_index: i32, + index: i64, + base_amount: i64, + price: i64, + trigger_price: i64, + integrator_account_index: i64, + integrator_taker_fee: i32, + integrator_maker_fee: i32, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn( + i32, i64, i64, i64, i64, + i64, i32, i32, u8, i64, i32, i64, + ) -> RawSignedTxResponse, + > = self.lib.get(b"SignModifyOrder\0").unwrap(); + raw_to_signed_tx(f( + market_index, index, base_amount, price, trigger_price, + integrator_account_index, integrator_taker_fee, integrator_maker_fee, + skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_transfer( + &self, + to_account_index: i64, + asset_index: i16, + from_route_type: u8, + to_route_type: u8, + amount: i64, + usdc_fee: i64, + memo: &str, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + let memo_c = CString::new(memo).unwrap(); + unsafe { + let f: Symbol< + unsafe extern "C" fn( + i64, i16, u8, u8, i64, i64, *mut c_char, + u8, i64, i32, i64, + ) -> RawSignedTxResponse, + > = self.lib.get(b"SignTransfer\0").unwrap(); + raw_to_signed_tx(f( + to_account_index, + asset_index, + from_route_type, + to_route_type, + amount, + usdc_fee, + memo_c.as_ptr() as *mut c_char, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn sign_create_public_pool( + &self, + operator_fee: i64, + initial_total_shares: i32, + min_operator_share_rate: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i64, i32, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignCreatePublicPool\0").unwrap(); + raw_to_signed_tx(f( + operator_fee, + initial_total_shares, + min_operator_share_rate, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn sign_update_public_pool( + &self, + public_pool_index: i64, + status: i32, + operator_fee: i64, + min_operator_share_rate: i32, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i64, i32, i64, i32, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignUpdatePublicPool\0").unwrap(); + raw_to_signed_tx(f( + public_pool_index, + status, + operator_fee, + min_operator_share_rate, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn sign_mint_shares( + &self, + public_pool_index: i64, + share_amount: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i64, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignMintShares\0").unwrap(); + raw_to_signed_tx(f( + public_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + pub fn sign_burn_shares( + &self, + public_pool_index: i64, + share_amount: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i64, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignBurnShares\0").unwrap(); + raw_to_signed_tx(f( + public_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + pub fn sign_update_leverage( + &self, + market_index: i32, + initial_margin_fraction: i32, + margin_mode: i32, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i32, i32, i32, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignUpdateLeverage\0").unwrap(); + raw_to_signed_tx(f( + market_index, + initial_margin_fraction, + margin_mode, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn create_auth_token( + &self, + deadline: i64, + api_key_index: i32, + account_index: i64, + ) -> StrOrErr { + unsafe { + let f: Symbol RawStrOrErr> = + self.lib.get(b"CreateAuthToken\0").unwrap(); + let raw = f(deadline, api_key_index, account_index); + StrOrErr { + value: ptr_to_string(raw.str_), + err: ptr_to_string(raw.err), + } + } + } + + pub fn sign_update_margin( + &self, + market_index: i32, + usdc_amount: i64, + direction: i32, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i32, i64, i32, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignUpdateMargin\0").unwrap(); + raw_to_signed_tx(f( + market_index, usdc_amount, direction, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + pub fn sign_stake_assets( + &self, + staking_pool_index: i64, + share_amount: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i64, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignStakeAssets\0").unwrap(); + raw_to_signed_tx(f( + staking_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + pub fn sign_unstake_assets( + &self, + staking_pool_index: i64, + share_amount: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn(i64, i64, u8, i64, i32, i64) -> RawSignedTxResponse, + > = self.lib.get(b"SignUnstakeAssets\0").unwrap(); + raw_to_signed_tx(f( + staking_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, + )) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_approve_integrator( + &self, + integrator_index: i64, + max_perps_taker_fee: u32, + max_perps_maker_fee: u32, + max_spot_taker_fee: u32, + max_spot_maker_fee: u32, + approval_expiry: i64, + skip_nonce: u8, + nonce: i64, + api_key_index: i32, + account_index: i64, + ) -> SignedTxResponse { + unsafe { + let f: Symbol< + unsafe extern "C" fn( + i64, u32, u32, u32, u32, i64, u8, i64, i32, i64, + ) -> RawSignedTxResponse, + > = self.lib.get(b"SignApproveIntegrator\0").unwrap(); + raw_to_signed_tx(f( + integrator_index, + max_perps_taker_fee, + max_perps_maker_fee, + max_spot_taker_fee, + max_spot_maker_fee, + approval_expiry, + skip_nonce, + nonce, + api_key_index, + account_index, + )) + } + } + + pub fn free(&self, ptr: *mut std::ffi::c_void) { + unsafe { + let f: Symbol = + self.lib.get(b"Free\0").unwrap(); + f(ptr); + } + } +} \ No newline at end of file diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs new file mode 100644 index 0000000..d2d130c --- /dev/null +++ b/examples/rust/src/main.rs @@ -0,0 +1,129 @@ +use std::sync::Arc; +use std::thread; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +use lighter_rust::LighterLib; + +const CHAIN_ID: i32 = 304; +const ACCOUNT_INDEX: i64 = 100; +const MARKET_INDEX: i32 = 0; // ETH market +const BASE_AMOUNT: i64 = 10_000; +const PRICE: i32 = 400_000; +const ORDER_TYPE: i32 = 0; // limit +const TIME_IN_FORCE: i32 = 2; // post-only +const N_THREADS: usize = 5; +const N_ORDERS: usize = 100; + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64 +} + +fn run_example(lib: &LighterLib, api_key_index: i32) { + // Generate a fresh API key pair + let api_resp = lib.generate_api_key(); + let (private_key, public_key) = match api_resp.check() { + Ok(v) => v, + Err(e) => { + eprintln!("[{}] GenerateAPIKey error: {}", api_key_index, e); + return; + } + }; + println!("[{}] publicKey={}", api_key_index, public_key); + + // Create a client bound to the generated key + if let Some(err) = lib.create_client(None, &private_key, CHAIN_ID, api_key_index, ACCOUNT_INDEX) + { + eprintln!("[{}] CreateClient error: {}", api_key_index, err); + return; + } + + // Auth token valid for 7 hours + let token_deadline = now_ms() + 7 * 60 * 60 * 1000; + let auth_token = match lib + .create_auth_token(token_deadline, api_key_index, ACCOUNT_INDEX) + .unwrap_value() + { + Ok(t) => t, + Err(e) => { + eprintln!("[{}] CreateAuthToken error: {}", api_key_index, e); + return; + } + }; + println!("[{}] authToken={}", api_key_index, auth_token); + + let mut nonce: i64 = 1; + let start = Instant::now(); + + for i in 1..=N_ORDERS { + let order_expiry = now_ms() + 60 * 60 * 1000; // 60 min from now + + // Sign a limit post-only ask order + let create = lib.sign_create_order( + MARKET_INDEX, + i as i64, // client_order_index + BASE_AMOUNT, + PRICE, + 1, // is_ask + ORDER_TYPE, + TIME_IN_FORCE, + 0, // reduce_only + 0, // trigger_price + order_expiry, + 0, // integrator_account_index + 0, // integrator_taker_fee + 0, // integrator_maker_fee + 0, // skip_nonce + nonce, + api_key_index, + ACCOUNT_INDEX, + ); + nonce += 1; + + if let Some(e) = create.err { + eprintln!("[{}] SignCreateOrder({}) error: {}", api_key_index, i, e); + } + + // Cancel the same order by client order index + let cancel = lib.sign_cancel_order( + MARKET_INDEX, + i as i64, + 0, // skip_nonce + nonce, + api_key_index, + ACCOUNT_INDEX, + ); + nonce += 1; + + if let Some(e) = cancel.err { + eprintln!("[{}] SignCancelOrder({}) error: {}", api_key_index, i, e); + } + } + + let elapsed = start.elapsed(); + println!( + "[{}] {} create+cancel pairs in {:.2} ms", + api_key_index, + N_ORDERS, + elapsed.as_secs_f64() * 1000.0 + ); +} + +fn main() { + let lib = Arc::new( + LighterLib::load_from_dir("../../sharedlib").expect("failed to load lighter shared library"), + ); + + let handles: Vec<_> = (0..N_THREADS) + .map(|i| { + let lib = Arc::clone(&lib); + thread::spawn(move || run_example(&lib, i as i32)) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } +} \ No newline at end of file diff --git a/scripts/test_wasm.mjs b/examples/wasm/test_wasm.mjs similarity index 99% rename from scripts/test_wasm.mjs rename to examples/wasm/test_wasm.mjs index 62b8d33..2211a8c 100644 --- a/scripts/test_wasm.mjs +++ b/examples/wasm/test_wasm.mjs @@ -4,7 +4,7 @@ import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(__dirname, "..", ".."); const buildDir = path.join(repoRoot, "build"); await import(path.join(buildDir, "wasm_exec.js")); diff --git a/justfile b/justfile index 9348a82..b3d4a94 100644 --- a/justfile +++ b/justfile @@ -46,4 +46,10 @@ build-windows-amd64-docker: build-wasm: go mod vendor - GOOS=js GOARCH=wasm go build -trimpath -o ./build/lighter-signer.wasm ./wasm/ \ No newline at end of file + GOOS=js GOARCH=wasm go build -trimpath -o ./build/lighter-signer.wasm ./wasm/ + +build-java: + mvn -B -f examples/java/pom.xml clean compile + +build-rust: + cargo build --release --manifest-path examples/rust/Cargo.toml From 1c535757ce1fbc3fff0d3c3609744a09c4dbdfe9 Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Thu, 23 Apr 2026 17:15:33 +0200 Subject: [PATCH 06/15] fixes --- .github/workflows/ci.yml | 46 +++++++++++++++++++--------------------- examples/cpp/example.cpp | 9 ++++++-- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 784e733..5021356 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,45 +2,46 @@ name: CI on: push: + branches: [ main ] pull_request: workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.23.1' + go-version-file: go.mod cache: true - - uses: actions/setup-node@v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 with: distribution: temurin java-version: '21' cache: maven - - uses: dtolnay/rust-toolchain@stable - - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: workspaces: examples/rust - - - uses: extractions/setup-just@v2 + - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - run: go test ./... - - run: just build-linux-local - - run: just build-wasm - name: Install wasm_exec.js @@ -53,10 +54,12 @@ jobs: run: node ./examples/wasm/test_wasm.mjs - run: just build-java - - run: just build-rust - - uses: actions/upload-artifact@v4 + - name: Compile C++ example + run: clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-linux.so -o ./build/example-cpp + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: lighter-go-linux-wasm path: | @@ -68,21 +71,16 @@ jobs: build-darwin: runs-on: macos-14 steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version-file: go.mod cache: true - - - uses: extractions/setup-just@v2 + - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - run: just build-darwin-local - - name: Compile C++ example - run: clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-darwin-arm64.dylib -o ./build/example-cpp - - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: lighter-go-darwin path: build/lighter-signer-darwin-arm64.dylib \ No newline at end of file diff --git a/examples/cpp/example.cpp b/examples/cpp/example.cpp index d5d5f45..1cffc19 100644 --- a/examples/cpp/example.cpp +++ b/examples/cpp/example.cpp @@ -3,8 +3,13 @@ #include #include #include -#include "../../build/lighter-signer-darwin-arm64.h" - +#if defined(__APPLE__) + #include "../../build/lighter-signer-darwin-arm64.h" +#elif defined(__linux__) + #include "../../build/lighter-signer-linux.h" +#elif defined(_WIN32) + #include "../../build/lighter-signer-windows.h" +#endif using namespace std; From 0b5b57ffdf9925d5d5d5d81152d469faa92abad2 Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Thu, 23 Apr 2026 17:20:30 +0200 Subject: [PATCH 07/15] fix --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5021356..db75fcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,10 +72,12 @@ jobs: runs-on: macos-14 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version-file: go.mod + go-version: '1.23.1' cache: true + env: + GOTOOLCHAIN: local - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - run: just build-darwin-local From 0fe9475fd8a8036ddee97f84836c19c8814a35af Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Thu, 23 Apr 2026 17:24:25 +0200 Subject: [PATCH 08/15] fix --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db75fcd..0cd99ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,9 +46,11 @@ jobs: - name: Install wasm_exec.js run: | - SRC="$(go env GOROOT)/lib/wasm/wasm_exec.js" - [ -f "$SRC" ] || SRC="$(go env GOROOT)/misc/wasm/wasm_exec.js" - cp "$SRC" ./build/wasm_exec.js + GO_VERSION=$(go env GOVERSION) + curl -fsSL "https://raw.githubusercontent.com/golang/go/${GO_VERSION}/lib/wasm/wasm_exec.js" \ + -o ./build/wasm_exec.js \ + || curl -fsSL "https://raw.githubusercontent.com/golang/go/${GO_VERSION}/misc/wasm/wasm_exec.js" \ + -o ./build/wasm_exec.js - name: Run WASM example run: node ./examples/wasm/test_wasm.mjs @@ -72,12 +74,10 @@ jobs: runs-on: macos-14 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.23.1' + go-version-file: go.mod cache: true - env: - GOTOOLCHAIN: local - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - run: just build-darwin-local From 92b7a9036afdfbb8cd560159db0bb35847eaee47 Mon Sep 17 00:00:00 2001 From: Alex Velea Date: Thu, 23 Apr 2026 19:15:08 +0300 Subject: [PATCH 09/15] cleanup & windows cross compile --- .github/workflows/ci.yml | 33 +++++++++++++++++++++------------ .gitignore | 1 - justfile | 5 +++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cd99ef..9f5db1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,13 +17,13 @@ jobs: build-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version-file: go.mod cache: true - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 @@ -38,13 +38,11 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: workspaces: examples/rust - - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - - run: go test ./... - run: just build-linux-local - run: just build-wasm - - name: Install wasm_exec.js + - name: Fetch wasm_exec.js run: | GO_VERSION=$(go env GOVERSION) curl -fsSL "https://raw.githubusercontent.com/golang/go/${GO_VERSION}/lib/wasm/wasm_exec.js" \ @@ -52,14 +50,23 @@ jobs: || curl -fsSL "https://raw.githubusercontent.com/golang/go/${GO_VERSION}/misc/wasm/wasm_exec.js" \ -o ./build/wasm_exec.js - - name: Run WASM example - run: node ./examples/wasm/test_wasm.mjs + - name: Cross-compile Windows DLL + run: | + sudo apt-get install -y gcc-mingw-w64-x86-64 + CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc \ + go build -buildmode=c-shared -trimpath \ + -o ./build/lighter-signer-windows-amd64.dll ./sharedlib/main.go + # make sure examples are building & test + - run: go test ./... + - run: just build-cpp - run: just build-java - run: just build-rust - - name: Compile C++ example - run: clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-linux.so -o ./build/example-cpp + - name: Run WASM example + run: node ./examples/wasm/test_wasm.mjs + - name: Run C++ example + run: LD_LIBRARY_PATH=./build ./build/example-cpp - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: @@ -67,22 +74,24 @@ jobs: path: | build/lighter-signer-linux.so build/lighter-signer-linux.h + build/lighter-signer-windows-amd64.dll + build/lighter-signer-windows-amd64.h build/lighter-signer.wasm build/wasm_exec.js build-darwin: runs-on: macos-14 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version-file: go.mod cache: true - - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - run: just build-darwin-local - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: lighter-go-darwin - path: build/lighter-signer-darwin-arm64.dylib \ No newline at end of file + path: build/lighter-signer-darwin-arm64.dylib diff --git a/.gitignore b/.gitignore index 1c8d418..195aa40 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ build/* !build/.keep /build - # Shared library build outputs sharedlib/lighter.dylib sharedlib/lighter.so diff --git a/justfile b/justfile index b3d4a94..127176c 100644 --- a/justfile +++ b/justfile @@ -48,8 +48,13 @@ build-wasm: go mod vendor GOOS=js GOARCH=wasm go build -trimpath -o ./build/lighter-signer.wasm ./wasm/ +### Examples + build-java: mvn -B -f examples/java/pom.xml clean compile build-rust: cargo build --release --manifest-path examples/rust/Cargo.toml + +build-cpp: + clang++ -std=c++20 -O3 examples/cpp/example.cpp ./build/lighter-signer-linux.so -o ./build/example-cpp \ No newline at end of file From d00ee6a03344d259aa801cb474b4fc54a70c581b Mon Sep 17 00:00:00 2001 From: Alex Velea Date: Thu, 23 Apr 2026 19:31:31 +0300 Subject: [PATCH 10/15] rename --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f5db1b..0852929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: true jobs: - build-and-test: + build-linux-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -70,7 +70,7 @@ jobs: - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: lighter-go-linux-wasm + name: lighter-go-linux-windows-wasm path: | build/lighter-signer-linux.so build/lighter-signer-linux.h From a2aaf3d66bb4643687b1a45f9aabb07e06377998 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 17:15:50 +0000 Subject: [PATCH 11/15] fix: free C-allocated strings in Rust, Java, and C++ FFI wrappers Every call to Sign*, GenerateAPIKey, CreateAuthToken, CreateClient, and CheckClient returns structs containing C.CString pointers (malloc'd by the Go shared library). The wrappers copied these strings into managed memory but never called the exported Free() function, leaking the originals. Rust: - Cache the Free symbol as a raw fn pointer on LighterLib::load(). - Pass free_fn into ptr_to_string() and raw_to_signed_tx() so every copied pointer is freed immediately after conversion. - All 22 affected methods now free automatically. Java: - Change String fields to Pointer in StrOrErr, ApiKeyResponse, and SignedTxResponse structs. - Add readAndFree(Lib) helper that reads the string and calls Lib.Free. - Update Example.java to use the new API. C++: - Add Free() calls for all returned char* fields in example.cpp. Co-Authored-By: mihai --- examples/cpp/example.cpp | 13 ++++ examples/java/src/main/java/Example.java | 34 +++++--- examples/java/src/main/java/LighterLib.java | 60 +++++++++----- examples/rust/src/lib.rs | 86 ++++++++++++--------- 4 files changed, 128 insertions(+), 65 deletions(-) diff --git a/examples/cpp/example.cpp b/examples/cpp/example.cpp index 1cffc19..563e940 100644 --- a/examples/cpp/example.cpp +++ b/examples/cpp/example.cpp @@ -32,18 +32,23 @@ void run_example(int apiKeyIndex) { ApiKeyResponse apiResp = GenerateAPIKey(); if (apiResp.err != nullptr) { + Free(apiResp.err); return; } CreateClient(nullptr, apiResp.privateKey, 304, apiKeyIndex, 100); + Free(apiResp.privateKey); + Free(apiResp.publicKey); long long accountIndex = 100; // create an auth token with expiry 7 hours in the future StrOrErr tokenResp = CreateAuthToken(now_ms() + 7 * 60 * 60 * 1000 , apiKeyIndex, accountIndex); if (tokenResp.err != nullptr) { + Free(tokenResp.err); return; } + Free(tokenResp.str); long long nonce = 1; @@ -64,7 +69,11 @@ void run_example(int apiKeyIndex) { if (create.err != nullptr) { cerr << "create" << '\t' << create.err << '\n'; + Free(create.err); } + if (create.txInfo != nullptr) Free(create.txInfo); + if (create.txHash != nullptr) Free(create.txHash); + if (create.messageToSign != nullptr) Free(create.messageToSign); // cancel order with client order id i on ETH market (market ID 0) auto cancel = SignCancelOrder(0, i, /* cSkipNonce */ 0, nonce, apiKeyIndex, accountIndex); @@ -72,7 +81,11 @@ void run_example(int apiKeyIndex) { if (cancel.err != nullptr) { cerr << "cancel" << '\t' << cancel.err << '\n'; + Free(cancel.err); } + if (cancel.txInfo != nullptr) Free(cancel.txInfo); + if (cancel.txHash != nullptr) Free(cancel.txHash); + if (cancel.messageToSign != nullptr) Free(cancel.messageToSign); } auto end = now_us(); cout << "elapsed" << '\t' << float(end - start) / 1000 << "ms" << '\n'; diff --git a/examples/java/src/main/java/Example.java b/examples/java/src/main/java/Example.java index 990aefa..e45fc83 100644 --- a/examples/java/src/main/java/Example.java +++ b/examples/java/src/main/java/Example.java @@ -27,14 +27,19 @@ public class Example { static void runExample(LighterLib.Lib lib, int apiKeyIndex) { // Generate a fresh API key pair LighterLib.ApiKeyResponse.ByValue apiResp = lib.GenerateAPIKey(); - if (apiResp.err != null) { - System.err.println("[" + apiKeyIndex + "] GenerateAPIKey error: " + apiResp.err); + String[] keys; + try { + keys = apiResp.readAndFree(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] GenerateAPIKey error: " + e.getMessage()); return; } - System.out.println("[" + apiKeyIndex + "] publicKey=" + apiResp.publicKey); + String privateKey = keys[0]; + String publicKey = keys[1]; + System.out.println("[" + apiKeyIndex + "] publicKey=" + publicKey); // Create a client bound to the generated key - String err = lib.CreateClient(null, apiResp.privateKey, CHAIN_ID, apiKeyIndex, ACCOUNT_INDEX); + String err = lib.CreateClient(null, privateKey, CHAIN_ID, apiKeyIndex, ACCOUNT_INDEX); if (err != null) { System.err.println("[" + apiKeyIndex + "] CreateClient error: " + err); return; @@ -43,11 +48,14 @@ static void runExample(LighterLib.Lib lib, int apiKeyIndex) { // Auth token valid for 7 hours long tokenDeadline = nowMs() + 7L * 60 * 60 * 1000; LighterLib.StrOrErr.ByValue tokenResp = lib.CreateAuthToken(tokenDeadline, apiKeyIndex, ACCOUNT_INDEX); - if (tokenResp.err != null) { - System.err.println("[" + apiKeyIndex + "] CreateAuthToken error: " + tokenResp.err); + String authToken; + try { + authToken = tokenResp.unwrap(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] CreateAuthToken error: " + e.getMessage()); return; } - System.out.println("[" + apiKeyIndex + "] authToken=" + tokenResp.str); + System.out.println("[" + apiKeyIndex + "] authToken=" + authToken); long nonce = 1L; long start = nowUs(); @@ -77,8 +85,10 @@ static void runExample(LighterLib.Lib lib, int apiKeyIndex) { ); nonce++; - if (create.err != null) { - System.err.println("[" + apiKeyIndex + "] SignCreateOrder(" + i + ") error: " + create.err); + try { + create.readAndFree(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] SignCreateOrder(" + i + ") error: " + e.getMessage()); } // Cancel the same order by client order index @@ -92,8 +102,10 @@ static void runExample(LighterLib.Lib lib, int apiKeyIndex) { ); nonce++; - if (cancel.err != null) { - System.err.println("[" + apiKeyIndex + "] SignCancelOrder(" + i + ") error: " + cancel.err); + try { + cancel.readAndFree(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] SignCancelOrder(" + i + ") error: " + e.getMessage()); } } diff --git a/examples/java/src/main/java/LighterLib.java b/examples/java/src/main/java/LighterLib.java index c5cab2e..86641e9 100644 --- a/examples/java/src/main/java/LighterLib.java +++ b/examples/java/src/main/java/LighterLib.java @@ -27,27 +27,34 @@ public class LighterLib { @FieldOrder({"str", "err"}) public static class StrOrErr extends Structure { - public String str; - public String err; + public Pointer str; + public Pointer err; public static class ByValue extends StrOrErr implements Structure.ByValue {} - public String unwrap() { - if (err != null) throw new RuntimeException(err); - return str; + public String unwrap(Lib lib) { + String errStr = readAndFree(lib, err); + String strStr = readAndFree(lib, str); + if (errStr != null) throw new RuntimeException(errStr); + return strStr; } } @FieldOrder({"privateKey", "publicKey", "err"}) public static class ApiKeyResponse extends Structure { - public String privateKey; - public String publicKey; - public String err; + public Pointer privateKey; + public Pointer publicKey; + public Pointer err; public static class ByValue extends ApiKeyResponse implements Structure.ByValue {} - public void check() { - if (err != null) throw new RuntimeException(err); + /** Read all string fields and free the native pointers. */ + public String[] readAndFree(Lib lib) { + String pk = LighterLib.readAndFree(lib, privateKey); + String pub_ = LighterLib.readAndFree(lib, publicKey); + String e = LighterLib.readAndFree(lib, err); + if (e != null) throw new RuntimeException(e); + return new String[]{pk, pub_}; } } @@ -55,17 +62,23 @@ public void check() { // 8-byte alignment on 64-bit platforms, so 7 bytes of padding follow txType. @FieldOrder({"txType", "_pad", "txInfo", "txHash", "messageToSign", "err"}) public static class SignedTxResponse extends Structure { - public byte txType; - public byte[] _pad = new byte[7]; - public String txInfo; - public String txHash; - public String messageToSign; - public String err; + public byte txType; + public byte[] _pad = new byte[7]; + public Pointer txInfo; + public Pointer txHash; + public Pointer messageToSign; + public Pointer err; public static class ByValue extends SignedTxResponse implements Structure.ByValue {} - public void check() { - if (err != null) throw new RuntimeException(err); + /** Read all string fields and free the native pointers. Returns {txInfo, txHash, messageToSign}. */ + public String[] readAndFree(Lib lib) { + String info = LighterLib.readAndFree(lib, txInfo); + String hash = LighterLib.readAndFree(lib, txHash); + String msg = LighterLib.readAndFree(lib, messageToSign); + String e = LighterLib.readAndFree(lib, err); + if (e != null) throw new RuntimeException(e); + return new String[]{info, hash, msg}; } } @@ -88,6 +101,17 @@ public static CreateOrderTxReq[] allocateArray(int size) { } } + // ------------------------------------------------------------------------- + // Helper — read a C string from a Pointer and free the native memory + // ------------------------------------------------------------------------- + + private static String readAndFree(Lib lib, Pointer p) { + if (p == null) return null; + String s = p.getString(0); + lib.Free(p); + return s; + } + // ------------------------------------------------------------------------- // JNA interface — all struct return values are ByValue // ------------------------------------------------------------------------- diff --git a/examples/rust/src/lib.rs b/examples/rust/src/lib.rs index 4d921bb..14b8753 100644 --- a/examples/rust/src/lib.rs +++ b/examples/rust/src/lib.rs @@ -110,22 +110,32 @@ impl SignedTxResponse { // Internal helpers // ------------------------------------------------------------------------- -unsafe fn ptr_to_string(ptr: *mut c_char) -> Option { +/// Copy the C string into a Rust `String` and free the original pointer +/// via the shared library's exported `Free` function. +unsafe fn ptr_to_string( + ptr: *mut c_char, + free_fn: unsafe extern "C" fn(*mut std::ffi::c_void), +) -> Option { if ptr.is_null() { None } else { - Some(CStr::from_ptr(ptr).to_string_lossy().into_owned()) + let s = CStr::from_ptr(ptr).to_string_lossy().into_owned(); + free_fn(ptr as *mut std::ffi::c_void); + Some(s) } } -fn raw_to_signed_tx(raw: RawSignedTxResponse) -> SignedTxResponse { +fn raw_to_signed_tx( + raw: RawSignedTxResponse, + free_fn: unsafe extern "C" fn(*mut std::ffi::c_void), +) -> SignedTxResponse { unsafe { SignedTxResponse { tx_type: raw.tx_type, - tx_info: ptr_to_string(raw.tx_info), - tx_hash: ptr_to_string(raw.tx_hash), - message_to_sign: ptr_to_string(raw.message_to_sign), - err: ptr_to_string(raw.err), + tx_info: ptr_to_string(raw.tx_info, free_fn), + tx_hash: ptr_to_string(raw.tx_hash, free_fn), + message_to_sign: ptr_to_string(raw.message_to_sign, free_fn), + err: ptr_to_string(raw.err, free_fn), } } } @@ -136,6 +146,7 @@ fn raw_to_signed_tx(raw: RawSignedTxResponse) -> SignedTxResponse { pub struct LighterLib { lib: Library, + free_fn: unsafe extern "C" fn(*mut std::ffi::c_void), } // The Go shared library uses its own goroutine scheduler and internal locking; @@ -147,7 +158,12 @@ impl LighterLib { /// Load by absolute path. pub fn load(path: &str) -> Result { let lib = unsafe { Library::new(path)? }; - Ok(Self { lib }) + let free_fn = unsafe { + let f: Symbol = + lib.get(b"Free\0")?; + *f + }; + Ok(Self { lib, free_fn }) } /// Load `lighter.{dylib,so}` from a directory relative to the working directory. @@ -169,9 +185,9 @@ impl LighterLib { self.lib.get(b"GenerateAPIKey\0").unwrap(); let raw = f(); ApiKeyResponse { - private_key: ptr_to_string(raw.private_key), - public_key: ptr_to_string(raw.public_key), - err: ptr_to_string(raw.err), + private_key: ptr_to_string(raw.private_key, self.free_fn), + public_key: ptr_to_string(raw.public_key, self.free_fn), + err: ptr_to_string(raw.err, self.free_fn), } } } @@ -200,7 +216,7 @@ impl LighterLib { chain_id, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -208,7 +224,7 @@ impl LighterLib { unsafe { let f: Symbol *mut c_char> = self.lib.get(b"CheckClient\0").unwrap(); - ptr_to_string(f(api_key_index, account_index)) + ptr_to_string(f(api_key_index, account_index), self.free_fn) } } @@ -231,7 +247,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -281,7 +297,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -316,7 +332,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -340,7 +356,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -360,7 +376,7 @@ impl LighterLib { > = self.lib.get(b"SignWithdraw\0").unwrap(); raw_to_signed_tx(f( asset_index, route_type, amount, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -375,7 +391,7 @@ impl LighterLib { let f: Symbol< unsafe extern "C" fn(u8, i64, i32, i64) -> RawSignedTxResponse, > = self.lib.get(b"SignCreateSubAccount\0").unwrap(); - raw_to_signed_tx(f(skip_nonce, nonce, api_key_index, account_index)) + raw_to_signed_tx(f(skip_nonce, nonce, api_key_index, account_index), self.free_fn) } } @@ -394,7 +410,7 @@ impl LighterLib { > = self.lib.get(b"SignCancelAllOrders\0").unwrap(); raw_to_signed_tx(f( time_in_force, time, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -425,7 +441,7 @@ impl LighterLib { market_index, index, base_amount, price, trigger_price, integrator_account_index, integrator_taker_fee, integrator_maker_fee, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -464,7 +480,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -490,7 +506,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -518,7 +534,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -537,7 +553,7 @@ impl LighterLib { > = self.lib.get(b"SignMintShares\0").unwrap(); raw_to_signed_tx(f( public_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -556,7 +572,7 @@ impl LighterLib { > = self.lib.get(b"SignBurnShares\0").unwrap(); raw_to_signed_tx(f( public_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -582,7 +598,7 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -597,8 +613,8 @@ impl LighterLib { self.lib.get(b"CreateAuthToken\0").unwrap(); let raw = f(deadline, api_key_index, account_index); StrOrErr { - value: ptr_to_string(raw.str_), - err: ptr_to_string(raw.err), + value: ptr_to_string(raw.str_, self.free_fn), + err: ptr_to_string(raw.err, self.free_fn), } } } @@ -619,7 +635,7 @@ impl LighterLib { > = self.lib.get(b"SignUpdateMargin\0").unwrap(); raw_to_signed_tx(f( market_index, usdc_amount, direction, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -638,7 +654,7 @@ impl LighterLib { > = self.lib.get(b"SignStakeAssets\0").unwrap(); raw_to_signed_tx(f( staking_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -657,7 +673,7 @@ impl LighterLib { > = self.lib.get(b"SignUnstakeAssets\0").unwrap(); raw_to_signed_tx(f( staking_pool_index, share_amount, skip_nonce, nonce, api_key_index, account_index, - )) + ), self.free_fn) } } @@ -692,15 +708,13 @@ impl LighterLib { nonce, api_key_index, account_index, - )) + ), self.free_fn) } } pub fn free(&self, ptr: *mut std::ffi::c_void) { unsafe { - let f: Symbol = - self.lib.get(b"Free\0").unwrap(); - f(ptr); + (self.free_fn)(ptr); } } } \ No newline at end of file From 754026ff758df87f99ca2e9349253b4297ee90c2 Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Sat, 25 Apr 2026 19:48:55 +0200 Subject: [PATCH 12/15] fixes --- .github/workflows/ci.yml | 1 + client/sign_test.go | 30 ++++++++++++++++++++++++------ examples/cpp/example.cpp | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0852929..54306a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,7 @@ jobs: - name: Cross-compile Windows DLL run: | + sudo apt-get update sudo apt-get install -y gcc-mingw-w64-x86-64 CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc \ go build -buildmode=c-shared -trimpath \ diff --git a/client/sign_test.go b/client/sign_test.go index f206129..283624c 100644 --- a/client/sign_test.go +++ b/client/sign_test.go @@ -89,7 +89,10 @@ func TestCreateClient(t *testing.T) { } func TestSignCancelOrder(t *testing.T) { - priv, _, _ := GenerateAPIKey() + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } c := newTestClient(t, priv) req := &types.CancelOrderTxReq{MarketIndex: 0, Index: 12345} @@ -133,7 +136,10 @@ func TestSignCancelOrder(t *testing.T) { } func TestSignCancelAllOrders(t *testing.T) { - priv, _, _ := GenerateAPIKey() + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } c := newTestClient(t, priv) req := &types.CancelAllOrdersTxReq{TimeInForce: 0, Time: 0} @@ -154,7 +160,10 @@ func TestSignCancelAllOrders(t *testing.T) { } func TestSignCreateOrder(t *testing.T) { - priv, _, _ := GenerateAPIKey() + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } c := newTestClient(t, priv) req := &types.CreateOrderTxReq{ @@ -192,7 +201,10 @@ func TestSignCreateOrder(t *testing.T) { } func TestSignCreateSubAccount(t *testing.T) { - priv, _, _ := GenerateAPIKey() + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } c := newTestClient(t, priv) tx, err := c.GetCreateSubAccountTransaction(opsWithSkipNonce(1, testNonce)) @@ -212,7 +224,10 @@ func TestSignCreateSubAccount(t *testing.T) { } func TestSignUpdateLeverage(t *testing.T) { - priv, _, _ := GenerateAPIKey() + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } c := newTestClient(t, priv) req := &types.UpdateLeverageTxReq{ @@ -237,7 +252,10 @@ func TestSignUpdateLeverage(t *testing.T) { } func TestSignCreateGroupedOrders(t *testing.T) { - priv, _, _ := GenerateAPIKey() + priv, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } c := newTestClient(t, priv) const expiry int64 = 7 * 24 * 60 * 60 * 1000 diff --git a/examples/cpp/example.cpp b/examples/cpp/example.cpp index 563e940..e5a0f06 100644 --- a/examples/cpp/example.cpp +++ b/examples/cpp/example.cpp @@ -36,7 +36,7 @@ void run_example(int apiKeyIndex) { return; } - CreateClient(nullptr, apiResp.privateKey, 304, apiKeyIndex, 100); + auto clientErr = CreateClient(nullptr, apiResp.privateKey, 304, apiKeyIndex, 100); Free(apiResp.privateKey); Free(apiResp.publicKey); From e79da3331fb8dbf94a8f1ddfb09490f8a4ef5aa2 Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Mon, 27 Apr 2026 11:41:49 +0200 Subject: [PATCH 13/15] fix CreateAuth token + memory leak --- examples/cpp/example.cpp | 2 +- examples/java/src/main/java/Example.java | 9 ++++++--- examples/java/src/main/java/LighterLib.java | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/cpp/example.cpp b/examples/cpp/example.cpp index e5a0f06..3935e68 100644 --- a/examples/cpp/example.cpp +++ b/examples/cpp/example.cpp @@ -43,7 +43,7 @@ void run_example(int apiKeyIndex) { long long accountIndex = 100; // create an auth token with expiry 7 hours in the future - StrOrErr tokenResp = CreateAuthToken(now_ms() + 7 * 60 * 60 * 1000 , apiKeyIndex, accountIndex); + StrOrErr tokenResp = CreateAuthToken(0 , apiKeyIndex, accountIndex); if (tokenResp.err != nullptr) { Free(tokenResp.err); return; diff --git a/examples/java/src/main/java/Example.java b/examples/java/src/main/java/Example.java index e45fc83..d6e387d 100644 --- a/examples/java/src/main/java/Example.java +++ b/examples/java/src/main/java/Example.java @@ -39,14 +39,17 @@ static void runExample(LighterLib.Lib lib, int apiKeyIndex) { System.out.println("[" + apiKeyIndex + "] publicKey=" + publicKey); // Create a client bound to the generated key - String err = lib.CreateClient(null, privateKey, CHAIN_ID, apiKeyIndex, ACCOUNT_INDEX); + String err = LighterLib.readAndFree( + lib, + lib.CreateClient(null, privateKey, CHAIN_ID, apiKeyIndex, ACCOUNT_INDEX) + ); if (err != null) { System.err.println("[" + apiKeyIndex + "] CreateClient error: " + err); return; } // Auth token valid for 7 hours - long tokenDeadline = nowMs() + 7L * 60 * 60 * 1000; + long tokenDeadline = 0; LighterLib.StrOrErr.ByValue tokenResp = lib.CreateAuthToken(tokenDeadline, apiKeyIndex, ACCOUNT_INDEX); String authToken; try { @@ -129,4 +132,4 @@ public static void main(String[] args) throws InterruptedException { t.join(); } } -} \ No newline at end of file +} diff --git a/examples/java/src/main/java/LighterLib.java b/examples/java/src/main/java/LighterLib.java index 86641e9..6815d62 100644 --- a/examples/java/src/main/java/LighterLib.java +++ b/examples/java/src/main/java/LighterLib.java @@ -105,7 +105,7 @@ public static CreateOrderTxReq[] allocateArray(int size) { // Helper — read a C string from a Pointer and free the native memory // ------------------------------------------------------------------------- - private static String readAndFree(Lib lib, Pointer p) { + public static String readAndFree(Lib lib, Pointer p) { if (p == null) return null; String s = p.getString(0); lib.Free(p); @@ -119,10 +119,10 @@ private static String readAndFree(Lib lib, Pointer p) { public interface Lib extends Library { ApiKeyResponse.ByValue GenerateAPIKey(); - String CreateClient(String url, String privateKey, int chainId, + Pointer CreateClient(String url, String privateKey, int chainId, int apiKeyIndex, long accountIndex); - String CheckClient(int apiKeyIndex, long accountIndex); + Pointer CheckClient(int apiKeyIndex, long accountIndex); SignedTxResponse.ByValue SignChangePubKey(String pubKey, byte skipNonce, long nonce, int apiKeyIndex, long accountIndex); @@ -227,4 +227,4 @@ public static Lib loadFromDir(String relativeDir) { .toAbsolutePath(); return load(lib.toString()); } -} \ No newline at end of file +} From 30fc54df7147823a4c1157a6d4fbc9514f17820e Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Mon, 27 Apr 2026 12:03:21 +0200 Subject: [PATCH 14/15] fix token deadline --- examples/cpp/example.cpp | 5 +++++ examples/rust/src/main.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/cpp/example.cpp b/examples/cpp/example.cpp index 3935e68..e424242 100644 --- a/examples/cpp/example.cpp +++ b/examples/cpp/example.cpp @@ -39,6 +39,11 @@ void run_example(int apiKeyIndex) { auto clientErr = CreateClient(nullptr, apiResp.privateKey, 304, apiKeyIndex, 100); Free(apiResp.privateKey); Free(apiResp.publicKey); + if (clientErr != nullptr) { + cerr << "CreateClient" << '\t' << clientErr << '\n'; + Free(clientErr); + return; + } long long accountIndex = 100; diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index d2d130c..1d33cbb 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -41,7 +41,7 @@ fn run_example(lib: &LighterLib, api_key_index: i32) { } // Auth token valid for 7 hours - let token_deadline = now_ms() + 7 * 60 * 60 * 1000; + let token_deadline = 0; let auth_token = match lib .create_auth_token(token_deadline, api_key_index, ACCOUNT_INDEX) .unwrap_value() From 7d0cf0041dba03e376a06539046df9af1fb365bc Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Tue, 28 Apr 2026 10:11:47 +0200 Subject: [PATCH 15/15] cpp improvement --- .github/workflows/ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54306a4..62330b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,16 +83,24 @@ jobs: build-darwin: runs-on: macos-14 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version-file: go.mod cache: true - run: just build-darwin-local - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - name: Compile C++ example + run: clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-darwin-arm64.dylib -o ./build/example-cpp + + - name: Run C++ example + run: DYLD_LIBRARY_PATH=./build ./build/example-cpp + + - run: go test ./... + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: lighter-go-darwin path: build/lighter-signer-darwin-arm64.dylib