Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

Comment thread
zen marked this conversation as resolved.
permissions:
contents: read

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: '1.23.1'
Comment thread
alexvelea marked this conversation as resolved.
Outdated
cache: true

- uses: actions/setup-node@v4
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 ./...

- 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

- 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:
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
Comment thread
zen marked this conversation as resolved.
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

- name: Compile C++ example
run: clang++ -std=c++20 -O3 ./examples/cpp/example.cpp ./build/lighter-signer-darwin-arm64.dylib -o ./build/example-cpp
Comment thread
alexvelea marked this conversation as resolved.

- uses: actions/upload-artifact@v4
with:
name: lighter-go-darwin
path: build/lighter-signer-darwin-arm64.dylib
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also would need:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

to prevent multiple pushes creating queue

Also for all external actions we need to pin them to SHA instead of tag (tags are mutable and prone to suplply chain attacks) .e.g.

uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

All actions with SHAs:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683       # v4
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff         # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020       # v4
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9       # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8   # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32      # v2
- uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6   # v2
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4

No release workflow right now. CI uploads artifacts, but nothing creates a GitHub Release on tag or attaches the binaries. Looks like the existing releases were done manually — might be worth automating at some point.

darwin/amd64 (Intel Mac) isn’t built in CI. macos-14 only covers arm64, so if you’re still shipping Intel dylibs, they’re not being verified.

Artifact retention isn’t explicitly set (so it defaults to 90 days). That’s fine for now, but could be worth setting it explicitly.

Running Java and Rust in the same Ubuntu job adds quite a bit of setup time. If builds start getting slow, it might make sense to split them into separate jobs.

14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,16 @@
vendor
build/*
!build/.keep
/build
/build


# Shared library build outputs
sharedlib/lighter.dylib
sharedlib/lighter.so
sharedlib/lighter.h

# Maven
examples/java/target/

# Rust
examples/rust/target/
282 changes: 282 additions & 0 deletions client/sign_test.go
Original file line number Diff line number Diff line change
@@ -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()
Comment thread
alexvelea marked this conversation as resolved.
Outdated
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)
}
Loading
Loading