diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..62330b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,106 @@ +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-linux-and-test: + runs-on: ubuntu-latest + steps: + - 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 + + - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: examples/rust + + - run: just build-linux-local + - run: just build-wasm + + - 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" \ + -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: 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 \ + -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: 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: + name: lighter-go-linux-windows-wasm + 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: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go.mod + cache: true + + - 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 + + - 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 diff --git a/.gitignore b/.gitignore index 54fa9d4..195aa40 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,15 @@ 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/client/sign_test.go b/client/sign_test.go new file mode 100644 index 0000000..283624c --- /dev/null +++ b/client/sign_test.go @@ -0,0 +1,300 @@ +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, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + 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, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + 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, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + 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, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + 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, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + 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, _, err := GenerateAPIKey() + if err != nil { + t.Fatalf("GenerateAPIKey error: %v", err) + } + 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) +} 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/cpp/example.cpp b/examples/cpp/example.cpp new file mode 100644 index 0000000..e424242 --- /dev/null +++ b/examples/cpp/example.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#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; + + +uint64_t now_us() { + using namespace std::chrono; + return duration_cast( + system_clock::now().time_since_epoch() + ).count(); +} + +uint64_t now_ms() { + using namespace std::chrono; + return duration_cast( + system_clock::now().time_since_epoch() + ).count(); +} + +void run_example(int apiKeyIndex) { + // Example: generate an API key + ApiKeyResponse apiResp = GenerateAPIKey(); + + if (apiResp.err != nullptr) { + Free(apiResp.err); + return; + } + + 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; + + // create an auth token with expiry 7 hours in the future + StrOrErr tokenResp = CreateAuthToken(0 , apiKeyIndex, accountIndex); + if (tokenResp.err != nullptr) { + Free(tokenResp.err); + return; + } + Free(tokenResp.str); + + long long nonce = 1; + + auto start = now_us(); + 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, + /* cIntegratorAccountIndex */ 0, + /* cIntegratorTakerFee */ 0, + /* cIntegratorMakerFee */ 0, + /* cSkipNonce */ 0, + nonce, apiKeyIndex, accountIndex); + nonce += 1; + + 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); + nonce += 1; + + 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'; +} + +int main() { + vector runners; + for (int i = 0; i < 5; i += 1) { + runners.emplace_back(run_example, i); + } + + for (auto& t: runners) { + t.join(); + } + + return 0; +} diff --git a/examples/example.cpp b/examples/example.cpp deleted file mode 100644 index 116704d..0000000 --- a/examples/example.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include -#include -#include -#include -#include -#include "../build/lighter-signer-darwin-arm64.h" - -using namespace std; - - -uint64_t now_us() { - using namespace std::chrono; - return duration_cast( - system_clock::now().time_since_epoch() - ).count(); -} - -uint64_t now_ms() { - using namespace std::chrono; - return duration_cast( - system_clock::now().time_since_epoch() - ).count(); -} - -void run_example(int apiKeyIndex) { - // Example: generate an API key - ApiKeyResponse apiResp = GenerateAPIKey(nullptr); - - if (apiResp.err != nullptr) { - return; - } - - CreateClient(nullptr, apiResp.privateKey, 304, apiKeyIndex, 100); - - 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) { - return; - } - - long long nonce = 1; - - auto start = now_us(); - 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); - nonce += 1; - - if (create.err != nullptr) { - cerr << "create" << '\t' << create.err << '\n'; - } - - // cancel order with client order id i on ETH market (market ID 0) - auto cancel = SignCancelOrder(0, i, nonce, apiKeyIndex, accountIndex); - nonce += 1; - - if (cancel.err != nullptr) { - cerr << "cancel" << '\t' << cancel.err << '\n'; - } - } - auto end = now_us(); - cout << "elapsed" << '\t' << float(end - start) / 1000 << "ms" << '\n'; -} - -int main() { - vector runners; - for (int i = 0; i < 5; i += 1) { - runners.emplace_back(run_example, i); - } - - for (auto& t: runners) { - t.join(); - } - - return 0; -} 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..d6e387d --- /dev/null +++ b/examples/java/src/main/java/Example.java @@ -0,0 +1,135 @@ +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(); + String[] keys; + try { + keys = apiResp.readAndFree(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] GenerateAPIKey error: " + e.getMessage()); + return; + } + String privateKey = keys[0]; + String publicKey = keys[1]; + System.out.println("[" + apiKeyIndex + "] publicKey=" + publicKey); + + // Create a client bound to the generated key + 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 = 0; + LighterLib.StrOrErr.ByValue tokenResp = lib.CreateAuthToken(tokenDeadline, apiKeyIndex, ACCOUNT_INDEX); + String authToken; + try { + authToken = tokenResp.unwrap(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] CreateAuthToken error: " + e.getMessage()); + return; + } + System.out.println("[" + apiKeyIndex + "] authToken=" + authToken); + + 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++; + + try { + create.readAndFree(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] SignCreateOrder(" + i + ") error: " + e.getMessage()); + } + + // 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++; + + try { + cancel.readAndFree(lib); + } catch (RuntimeException e) { + System.err.println("[" + apiKeyIndex + "] SignCancelOrder(" + i + ") error: " + e.getMessage()); + } + } + + 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(); + } + } +} diff --git a/examples/java/src/main/java/LighterLib.java b/examples/java/src/main/java/LighterLib.java new file mode 100644 index 0000000..6815d62 --- /dev/null +++ b/examples/java/src/main/java/LighterLib.java @@ -0,0 +1,230 @@ +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 Pointer str; + public Pointer err; + + public static class ByValue extends StrOrErr implements Structure.ByValue {} + + 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 Pointer privateKey; + public Pointer publicKey; + public Pointer err; + + public static class ByValue extends ApiKeyResponse implements Structure.ByValue {} + + /** 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_}; + } + } + + // 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 Pointer txInfo; + public Pointer txHash; + public Pointer messageToSign; + public Pointer err; + + public static class ByValue extends SignedTxResponse implements Structure.ByValue {} + + /** 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}; + } + } + + @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); + } + } + + // ------------------------------------------------------------------------- + // Helper — read a C string from a Pointer and free the native memory + // ------------------------------------------------------------------------- + + public 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 + // ------------------------------------------------------------------------- + + public interface Lib extends Library { + ApiKeyResponse.ByValue GenerateAPIKey(); + + Pointer CreateClient(String url, String privateKey, int chainId, + int apiKeyIndex, long accountIndex); + + Pointer 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()); + } +} 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..14b8753 --- /dev/null +++ b/examples/rust/src/lib.rs @@ -0,0 +1,720 @@ +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 +// ------------------------------------------------------------------------- + +/// 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 { + 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, + 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, 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), + } + } +} + +// ------------------------------------------------------------------------- +// LighterLib — loads the shared library dynamically (mirrors Java JNA usage) +// ------------------------------------------------------------------------- + +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; +// 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)? }; + 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. + 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, self.free_fn), + public_key: ptr_to_string(raw.public_key, self.free_fn), + err: ptr_to_string(raw.err, self.free_fn), + } + } + } + + /// 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, + ), self.free_fn) + } + } + + 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), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + #[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, + ), self.free_fn) + } + } + + #[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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + #[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, + ), self.free_fn) + } + } + + #[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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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_, self.free_fn), + err: ptr_to_string(raw.err, self.free_fn), + } + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + 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, + ), self.free_fn) + } + } + + #[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, + ), self.free_fn) + } + } + + pub fn free(&self, ptr: *mut std::ffi::c_void) { + unsafe { + (self.free_fn)(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..1d33cbb --- /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 = 0; + 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/examples/wasm/test_wasm.mjs b/examples/wasm/test_wasm.mjs new file mode 100644 index 0000000..2211a8c --- /dev/null +++ b/examples/wasm/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 ---"); diff --git a/justfile b/justfile index 9348a82..127176c 100644 --- a/justfile +++ b/justfile @@ -46,4 +46,15 @@ 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/ + +### 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