diff --git a/.gitignore b/.gitignore index 93fc83a..feffc91 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +build/** + +examples/c-app/bin/** diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..62da515 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +GOOS := $(shell go env GOOS) +LIBNAME := libgowalletsdk +BUILD_DIR := build + +ifeq ($(GOOS),darwin) + LIBEXT := .dylib +else + LIBEXT := .so +endif + +.PHONY: build-c-lib clean check-go + +check-go: + @current=$$(go version | awk '{print $$3}' | sed 's/go//'); \ + required=1.23; \ + if [ -z "$$current" ]; then \ + echo "Unable to detect Go version. Please install Go $$required or newer."; \ + exit 1; \ + fi; \ + # Compare versions using sort -V + if [ $$(printf '%s\n' "$$required" "$$current" | sort -V | head -n1) != "$$required" ]; then \ + echo "Go $$required or newer is required. Found $$current"; \ + echo "Tip: brew install go (or ensure PATH uses a recent Go)"; \ + exit 1; \ + fi + +build-c-lib: check-go + mkdir -p $(BUILD_DIR) + go build -buildmode=c-shared -o $(BUILD_DIR)/$(LIBNAME)$(LIBEXT) ./cshared + @echo "Built $(BUILD_DIR)/$(LIBNAME)$(LIBEXT) and header $(BUILD_DIR)/$(LIBNAME).h" + +clean: + rm -f $(BUILD_DIR)/$(LIBNAME).so $(BUILD_DIR)/$(LIBNAME).dylib $(BUILD_DIR)/$(LIBNAME).h + diff --git a/cshared/lib.go b/cshared/lib.go new file mode 100644 index 0000000..38870ef --- /dev/null +++ b/cshared/lib.go @@ -0,0 +1,131 @@ +package main + +/* +#include +*/ +import "C" + +import ( + "context" + "sync" + "unsafe" + + "github.com/ethereum/go-ethereum/common" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + sdkethclient "github.com/status-im/go-wallet-sdk/pkg/ethclient" +) + +var ( + clientsMutex sync.RWMutex + nextHandle uint64 = 1 + clients = map[uint64]*sdkethclient.Client{} +) + +func storeClient(c *sdkethclient.Client) uint64 { + clientsMutex.Lock() + defer clientsMutex.Unlock() + h := nextHandle + nextHandle++ + clients[h] = c + return h +} + +func getClient(handle uint64) *sdkethclient.Client { + clientsMutex.RLock() + defer clientsMutex.RUnlock() + return clients[handle] +} + +func deleteClient(handle uint64) { + clientsMutex.Lock() + defer clientsMutex.Unlock() + delete(clients, handle) +} + +//export GoWSK_NewClient +func GoWSK_NewClient(rpcURL *C.char, errOut **C.char) C.ulonglong { + if rpcURL == nil { + if errOut != nil { + *errOut = C.CString("rpcURL is NULL") + } + return 0 + } + url := C.GoString(rpcURL) + rpcClient, err := gethrpc.Dial(url) + if err != nil { + if errOut != nil { + *errOut = C.CString(err.Error()) + } + return 0 + } + client := sdkethclient.NewClient(rpcClient) + handle := storeClient(client) + return C.ulonglong(handle) +} + +//export GoWSK_CloseClient +func GoWSK_CloseClient(handle C.ulonglong) { + h := uint64(handle) + c := getClient(h) + if c != nil { + c.Close() + deleteClient(h) + } +} + +//export GoWSK_ChainID +func GoWSK_ChainID(handle C.ulonglong, errOut **C.char) *C.char { + c := getClient(uint64(handle)) + if c == nil { + if errOut != nil { + *errOut = C.CString("invalid client handle") + } + return nil + } + id, err := c.EthChainId(context.Background()) + if err != nil { + if errOut != nil { + *errOut = C.CString(err.Error()) + } + return nil + } + return C.CString(id.String()) +} + +//export GoWSK_GetBalance +func GoWSK_GetBalance(handle C.ulonglong, address *C.char, errOut **C.char) *C.char { + c := getClient(uint64(handle)) + if c == nil { + if errOut != nil { + *errOut = C.CString("invalid client handle") + } + return nil + } + if address == nil { + if errOut != nil { + *errOut = C.CString("address is NULL") + } + return nil + } + addr := common.HexToAddress(C.GoString(address)) + bal, err := c.EthGetBalance(context.Background(), addr, nil) + if err != nil { + if errOut != nil { + *errOut = C.CString(err.Error()) + } + return nil + } + return C.CString(bal.String()) +} + +// frees C strings returned by GoWSK functions to prevent memory leaks. +// +//export GoWSK_FreeCString +func GoWSK_FreeCString(s *C.char) { + if s != nil { + C.free(unsafe.Pointer(s)) + } +} + +func main() {} diff --git a/docs/specs.md b/docs/specs.md index 236b5c8..d96fd01 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -11,7 +11,8 @@ Go Wallet SDK is a modular Go library intended to support the development of m | `pkg/ethclient` | Chain‑agnostic Ethereum JSON‑RPC client. It provides two method sets: a drop‑in replacement compatible with go‑ethereum’s `ethclient` and a custom implementation that follows the Ethereum JSON‑RPC specification without assuming chain‑specific types. It supports JSON‑RPC methods covering `eth_`, `net_` and `web3_` namespace | | `pkg/common` | Shared types and constants. Such as canonical chain IDs (e.g., Ethereum Mainnet, Optimism, Arbitrum, BSC, Base). Developers use these values when configuring the SDK or examples. | | `pkg/balance/contracts` | Solidity contracts (not part of the published source) used by the balance fetcher when interacting with on‑chain balance scanning contracts. | -| `examples/` | Demonstrations of SDK usage. Includes `balance-fetcher-web` (a web interface for batch balance fetching) and `ethclient‑usage` (an example that exercises the Ethereum client across multiple RPC endpoints). | | +| `cshared/` | C shared library bindings that expose core SDK functionality to C applications. | +| `examples/` | Demonstrations of SDK usage. Includes `balance-fetcher-web` (a web interface for batch balance fetching), `ethclient‑usage` (an example that exercises the Ethereum client across multiple RPC endpoints), and `c-app` (a C application demonstranting usage of the C library usage). | | ## 2. Architecture @@ -45,6 +46,11 @@ Internally, the client stores a reference to an RPC client and implements each m The `pkg/common` package defines shared types and enumerations. The main export is `type ChainID uint64` with constants for well‑known networks such as `EthereumMainnet`, `EthereumSepolia`, `OptimismMainnet`, `ArbitrumMainnet`, `BSCMainnet`, `BaseMainnet`, `BaseSepolia` and a custom `StatusNetworkSepolia`. These constants allow the examples to pre‑populate supported chains and label results without repeating numeric IDs. +### 2.5 C Library + +At `cshared/lib.go` the library functions are exposed to be used as C bindings for core SDK functionality, enabling integration with C applications and other languages that can interface with C libraries. +The shared library is built using Go's `c-shared` build mode (e.g `go build -buildmode=c-shared -o lib.so lib.go`), which generates both the library file (`.so` on Linux, `.dylib` on macOS) and a corresponding C header file with function declarations and type definitions. + ## 3. API Description ### 3.1 Balance Fetcher API (`pkg/balance/fetcher`) @@ -255,6 +261,53 @@ Converts Go `ethereum.FilterQuery` structs into JSON-RPC filter objects: This enables `EthGetLogs`, `EthNewFilter`, and other event filtering methods to work correctly across all EVM chains. +### 3.3 C Shared Library API (`cshared/`) + +The C shared library provides a minimal but complete interface for blockchain operations from C applications. All functions use consistent patterns for error handling and memory management. + +| Function | Description | Parameters | Returns | +| -------- | ----------- | ---------- | ------- | +| `GoWSK_NewClient(rpcURL, errOut)` | Creates a new Ethereum client connected to the specified RPC endpoint | `rpcURL`: null-terminated string with RPC URL; `errOut`: optional double pointer for error message | Opaque client handle (0 on failure) | +| `GoWSK_CloseClient(handle)` | Closes an Ethereum client and releases its resources | `handle`: client handle from `GoWSK_NewClient` | None | +| `GoWSK_ChainID(handle, errOut)` | Retrieves the chain ID for the connected network | `handle`: client handle; `errOut`: optional double pointer for error message | Chain ID as null-terminated string (must be freed) | +| `GoWSK_GetBalance(handle, address, errOut)` | Fetches the native token balance for an address | `handle`: client handle; `address`: hex-encoded Ethereum address; `errOut`: optional double pointer for error message | Balance in wei as null-terminated string (must be freed) | +| `GoWSK_FreeCString(s)` | Frees a string allocated by the library | `s`: string pointer returned by other functions | None | + +**Usage Pattern** + +All C applications follow the same basic pattern: + +```c +#include "libgowalletsdk.h" + +// Create client +char* err = NULL; +unsigned long long client = GoWSK_NewClient("https://mainnet.infura.io/v3/KEY", &err); +if (client == 0) { + fprintf(stderr, "Error: %s\n", err); + GoWSK_FreeCString(err); + return 1; +} + +// Use client APIs +char* chainID = GoWSK_ChainID(client, &err); +if (chainID) { + printf("Chain ID: %s\n", chainID); + GoWSK_FreeCString(chainID); +} + +char* balance = GoWSK_GetBalance(client, "0x...", &err); +if (balance) { + printf("Balance: %s wei\n", balance); + GoWSK_FreeCString(balance); +} + +// Always close client +GoWSK_CloseClient(client); +``` + +All string returns from the library are allocated with `malloc` and must be freed using `GoWSK_FreeCString`. Also Error messages returned via `errOut` parameters must also be freed + ## 4. Example Applications ### 4.1 Web‑Based Balance Fetcher @@ -277,6 +330,27 @@ The `examples/ethclient-usage` folder shows how to use the Ethereum client acros - **Code Structure** – The example is split into `main.go`, which loops over endpoints, and helper functions such as `testRPC()` that call various methods and handle errors. +### 4.3 C Application Example + +At `examples/c-app` there is a simple app demonstrating how to use the C library. + +**usage** + +At the root do to create the library: + +```bash +make build-c-lib +``` + +Run the example: + +```bash +cd examples/c-app && make build +make +cd bin/ +./c-app +``` + ## 5. Testing & Development ### 5.1 Fetching SDK @@ -297,6 +371,20 @@ go test ./... This executes unit tests for the balance fetcher and Ethereum client. The balance fetcher includes a `mock` package to simulate RPC responses. The repository also includes continuous integration workflows (`.github/workflows`) and static analysis configurations (`.golangci.yml`). +### 5.3 Building the C Shared Library + +The SDK includes build support for creating C shared libraries that expose core functionality to non-Go applications. + +To build the library run: + +```bash +make build-c-lib +``` + +This creates: +- `build/libgowalletsdk.dylib` (macOS) or `build/libgowalletsdk.so` (Linux) +- `build/libgowalletsdk.h` (C header file) + ## 6. Limitations & Future Improvements - diff --git a/examples/c-app/Makefile b/examples/c-app/Makefile new file mode 100644 index 0000000..59c65e1 --- /dev/null +++ b/examples/c-app/Makefile @@ -0,0 +1,30 @@ +CC := cc +ROOT := ../.. +BUILD_DIR := $(ROOT)/build +LIB := $(BUILD_DIR)/libgowalletsdk +BIN_DIR := bin + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + LIBEXT := .dylib + RPATH := -Wl,-rpath,@executable_path/../bin +else + LIBEXT := .so + RPATH := -Wl,-rpath,$(BIN_DIR) +endif + +LIBFILE = $(LIB)$(LIBEXT) + +.PHONY: run build clean + +build: + mkdir -p $(BIN_DIR) + $(MAKE) -C $(ROOT) build-c-lib + cp -f $(LIBFILE) $(BIN_DIR)/ + $(CC) -I$(BUILD_DIR) -L$(BIN_DIR) -o $(BIN_DIR)/c-app main.c -lgowalletsdk $(RPATH) + +run: build + $(BIN_DIR)/c-app + +clean: + rm -rf $(BIN_DIR) diff --git a/examples/c-app/README.md b/examples/c-app/README.md new file mode 100644 index 0000000..fc06a84 --- /dev/null +++ b/examples/c-app/README.md @@ -0,0 +1,22 @@ +# C example using the Go Wallet SDK shared library + +Build steps: +- From repo root: make build-c-lib +- Then: cd examples/c-app && make build + +Run the example: +```bash +make +cd bin/ +./c-app +``` + +Notes: +- The build generates build/libgowalletsdk.(so|dylib) and header build/libgowalletsdk.h at the repo root. +- On macOS, the example copies the dylib next to the executable and sets rpath for convenience. +- Exported functions: + - GoWSK_NewClient(const char* rpcURL, char** errOut) -> unsigned long long + - GoWSK_CloseClient(unsigned long long handle) + - GoWSK_ChainID(unsigned long long handle, char** errOut) -> char* + - GoWSK_GetBalance(unsigned long long handle, const char* address, char** errOut) -> char* + - GoWSK_FreeCString(char* s) diff --git a/examples/c-app/main.c b/examples/c-app/main.c new file mode 100644 index 0000000..6e4507c --- /dev/null +++ b/examples/c-app/main.c @@ -0,0 +1,42 @@ +#include +#include +#include + +// Header generated by Go build -buildmode=c-shared +#include "libgowalletsdk.h" + +int main(int argc, char** argv) { + const char* url = "https://ethereum-rpc.publicnode.com"; + const char* addr = "0x0000000000000000000000000000000000000000"; + + char* err = NULL; + unsigned long long h = GoWSK_NewClient((char*)url, &err); + if (h == 0) { + fprintf(stderr, "Failed to create client: %s\n", err ? err : "unknown error"); + if (err) GoWSK_FreeCString(err); + return 1; + } + + char* chain = GoWSK_ChainID(h, &err); + if (chain == NULL) { + fprintf(stderr, "ChainID error: %s\n", err ? err : "unknown error"); + if (err) GoWSK_FreeCString(err); + GoWSK_CloseClient(h); + return 1; + } + printf("ChainID: %s\n", chain); + GoWSK_FreeCString(chain); + + char* balance = GoWSK_GetBalance(h, (char*)addr, &err); + if (balance == NULL) { + fprintf(stderr, "GetBalance error: %s\n", err ? err : "unknown error"); + if (err) GoWSK_FreeCString(err); + GoWSK_CloseClient(h); + return 1; + } + printf("Balance(wei): %s\n", balance); + GoWSK_FreeCString(balance); + + GoWSK_CloseClient(h); + return 0; +}