Skip to content

Commit 0efa824

Browse files
committed
feat(cshared/,examples/c-app): Support compiling wallet SDK as a C library; add simple example
feat(cshared/,examples/c-app): Support compiling wallet SDK as a C library; add simple example feat(cshared/,examples/c-app): Support compiling wallet SDK as a C library; add simple example initial attempt some fixes; docs more fixes fix linting errors
1 parent 63a2607 commit 0efa824

File tree

7 files changed

+352
-1
lines changed

7 files changed

+352
-1
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ go.work.sum
2828
# Editor/IDE
2929
# .idea/
3030
# .vscode/
31+
32+
build/**
33+
34+
examples/c-app/bin/**

Makefile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
GOOS := $(shell go env GOOS)
2+
LIBNAME := libgowalletsdk
3+
BUILD_DIR := build
4+
5+
ifeq ($(GOOS),darwin)
6+
LIBEXT := .dylib
7+
else
8+
LIBEXT := .so
9+
endif
10+
11+
.PHONY: build-c-lib clean check-go
12+
13+
check-go:
14+
@current=$$(go version | awk '{print $$3}' | sed 's/go//'); \
15+
required=1.23; \
16+
if [ -z "$$current" ]; then \
17+
echo "Unable to detect Go version. Please install Go $$required or newer."; \
18+
exit 1; \
19+
fi; \
20+
# Compare versions using sort -V
21+
if [ $$(printf '%s\n' "$$required" "$$current" | sort -V | head -n1) != "$$required" ]; then \
22+
echo "Go $$required or newer is required. Found $$current"; \
23+
echo "Tip: brew install go (or ensure PATH uses a recent Go)"; \
24+
exit 1; \
25+
fi
26+
27+
build-c-lib: check-go
28+
mkdir -p $(BUILD_DIR)
29+
go build -buildmode=c-shared -o $(BUILD_DIR)/$(LIBNAME)$(LIBEXT) ./cshared
30+
@echo "Built $(BUILD_DIR)/$(LIBNAME)$(LIBEXT) and header $(BUILD_DIR)/$(LIBNAME).h"
31+
32+
clean:
33+
rm -f $(BUILD_DIR)/$(LIBNAME).so $(BUILD_DIR)/$(LIBNAME).dylib $(BUILD_DIR)/$(LIBNAME).h
34+

cshared/lib.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package main
2+
3+
/*
4+
#include <stdlib.h>
5+
*/
6+
import "C"
7+
8+
import (
9+
"context"
10+
"sync"
11+
"unsafe"
12+
13+
"github.com/ethereum/go-ethereum/common"
14+
gethrpc "github.com/ethereum/go-ethereum/rpc"
15+
16+
sdkethclient "github.com/status-im/go-wallet-sdk/pkg/ethclient"
17+
)
18+
19+
var (
20+
clientsMutex sync.RWMutex
21+
nextHandle uint64 = 1
22+
clients = map[uint64]*sdkethclient.Client{}
23+
)
24+
25+
func storeClient(c *sdkethclient.Client) uint64 {
26+
clientsMutex.Lock()
27+
defer clientsMutex.Unlock()
28+
h := nextHandle
29+
nextHandle++
30+
clients[h] = c
31+
return h
32+
}
33+
34+
func getClient(handle uint64) *sdkethclient.Client {
35+
clientsMutex.RLock()
36+
defer clientsMutex.RUnlock()
37+
return clients[handle]
38+
}
39+
40+
func deleteClient(handle uint64) {
41+
clientsMutex.Lock()
42+
defer clientsMutex.Unlock()
43+
delete(clients, handle)
44+
}
45+
46+
//export GoWSK_NewClient
47+
func GoWSK_NewClient(rpcURL *C.char, errOut **C.char) C.ulonglong {
48+
if rpcURL == nil {
49+
if errOut != nil {
50+
*errOut = C.CString("rpcURL is NULL")
51+
}
52+
return 0
53+
}
54+
url := C.GoString(rpcURL)
55+
rpcClient, err := gethrpc.Dial(url)
56+
if err != nil {
57+
if errOut != nil {
58+
*errOut = C.CString(err.Error())
59+
}
60+
return 0
61+
}
62+
client := sdkethclient.NewClient(rpcClient)
63+
handle := storeClient(client)
64+
return C.ulonglong(handle)
65+
}
66+
67+
//export GoWSK_CloseClient
68+
func GoWSK_CloseClient(handle C.ulonglong) {
69+
h := uint64(handle)
70+
c := getClient(h)
71+
if c != nil {
72+
c.Close()
73+
deleteClient(h)
74+
}
75+
}
76+
77+
//export GoWSK_ChainID
78+
func GoWSK_ChainID(handle C.ulonglong, errOut **C.char) *C.char {
79+
c := getClient(uint64(handle))
80+
if c == nil {
81+
if errOut != nil {
82+
*errOut = C.CString("invalid client handle")
83+
}
84+
return nil
85+
}
86+
id, err := c.EthChainId(context.Background())
87+
if err != nil {
88+
if errOut != nil {
89+
*errOut = C.CString(err.Error())
90+
}
91+
return nil
92+
}
93+
return C.CString(id.String())
94+
}
95+
96+
//export GoWSK_GetBalance
97+
func GoWSK_GetBalance(handle C.ulonglong, address *C.char, errOut **C.char) *C.char {
98+
c := getClient(uint64(handle))
99+
if c == nil {
100+
if errOut != nil {
101+
*errOut = C.CString("invalid client handle")
102+
}
103+
return nil
104+
}
105+
if address == nil {
106+
if errOut != nil {
107+
*errOut = C.CString("address is NULL")
108+
}
109+
return nil
110+
}
111+
addr := common.HexToAddress(C.GoString(address))
112+
bal, err := c.EthGetBalance(context.Background(), addr, nil)
113+
if err != nil {
114+
if errOut != nil {
115+
*errOut = C.CString(err.Error())
116+
}
117+
return nil
118+
}
119+
return C.CString(bal.String())
120+
}
121+
122+
// frees C strings returned by GoWSK functions to prevent memory leaks.
123+
//
124+
//export GoWSK_FreeCString
125+
func GoWSK_FreeCString(s *C.char) {
126+
if s != nil {
127+
C.free(unsafe.Pointer(s))
128+
}
129+
}
130+
131+
func main() {}

docs/specs.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ Go Wallet SDK is a modular Go library intended to support the development of m
1111
| `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 |
1212
| `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. |
1313
| `pkg/balance/contracts` | Solidity contracts (not part of the published source) used by the balance fetcher when interacting with on‑chain balance scanning contracts. |
14-
| `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). | |
14+
| `cshared/` | C shared library bindings that expose core SDK functionality to C applications. |
15+
| `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). | |
1516

1617
## 2. Architecture
1718

@@ -45,6 +46,11 @@ Internally, the client stores a reference to an RPC client and implements each m
4546

4647
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.
4748

49+
### 2.5 C Library
50+
51+
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.
52+
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.
53+
4854
## 3. API Description
4955

5056
### 3.1 Balance Fetcher API (`pkg/balance/fetcher`)
@@ -255,6 +261,53 @@ Converts Go `ethereum.FilterQuery` structs into JSON-RPC filter objects:
255261

256262
This enables `EthGetLogs`, `EthNewFilter`, and other event filtering methods to work correctly across all EVM chains.
257263

264+
### 3.3 C Shared Library API (`cshared/`)
265+
266+
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.
267+
268+
| Function | Description | Parameters | Returns |
269+
| -------- | ----------- | ---------- | ------- |
270+
| `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) |
271+
| `GoWSK_CloseClient(handle)` | Closes an Ethereum client and releases its resources | `handle`: client handle from `GoWSK_NewClient` | None |
272+
| `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) |
273+
| `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) |
274+
| `GoWSK_FreeCString(s)` | Frees a string allocated by the library | `s`: string pointer returned by other functions | None |
275+
276+
**Usage Pattern**
277+
278+
All C applications follow the same basic pattern:
279+
280+
```c
281+
#include "libgowalletsdk.h"
282+
283+
// Create client
284+
char* err = NULL;
285+
unsigned long long client = GoWSK_NewClient("https://mainnet.infura.io/v3/KEY", &err);
286+
if (client == 0) {
287+
fprintf(stderr, "Error: %s\n", err);
288+
GoWSK_FreeCString(err);
289+
return 1;
290+
}
291+
292+
// Use client APIs
293+
char* chainID = GoWSK_ChainID(client, &err);
294+
if (chainID) {
295+
printf("Chain ID: %s\n", chainID);
296+
GoWSK_FreeCString(chainID);
297+
}
298+
299+
char* balance = GoWSK_GetBalance(client, "0x...", &err);
300+
if (balance) {
301+
printf("Balance: %s wei\n", balance);
302+
GoWSK_FreeCString(balance);
303+
}
304+
305+
// Always close client
306+
GoWSK_CloseClient(client);
307+
```
308+
309+
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
310+
258311
## 4. Example Applications
259312
260313
### 4.1 Web‑Based Balance Fetcher
@@ -277,6 +330,27 @@ The `examples/ethclient-usage` folder shows how to use the Ethereum client acros
277330
278331
- **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.
279332
333+
### 4.3 C Application Example
334+
335+
At `examples/c-app` there is a simple app demonstrating how to use the C library.
336+
337+
**usage**
338+
339+
At the root do to create the library:
340+
341+
```bash
342+
make build-c-lib
343+
```
344+
345+
Run the example:
346+
347+
```bash
348+
cd examples/c-app && make build
349+
make
350+
cd bin/
351+
./c-app
352+
```
353+
280354
## 5. Testing & Development
281355

282356
### 5.1 Fetching SDK
@@ -297,6 +371,20 @@ go test ./...
297371

298372
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`).
299373

374+
### 5.3 Building the C Shared Library
375+
376+
The SDK includes build support for creating C shared libraries that expose core functionality to non-Go applications.
377+
378+
To build the library run:
379+
380+
```bash
381+
make build-c-lib
382+
```
383+
384+
This creates:
385+
- `build/libgowalletsdk.dylib` (macOS) or `build/libgowalletsdk.so` (Linux)
386+
- `build/libgowalletsdk.h` (C header file)
387+
300388
## 6. Limitations & Future Improvements
301389

302390
-

examples/c-app/Makefile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
CC := cc
2+
ROOT := ../..
3+
BUILD_DIR := $(ROOT)/build
4+
LIB := $(BUILD_DIR)/libgowalletsdk
5+
BIN_DIR := bin
6+
7+
UNAME_S := $(shell uname -s)
8+
ifeq ($(UNAME_S),Darwin)
9+
LIBEXT := .dylib
10+
RPATH := -Wl,-rpath,@executable_path/../bin
11+
else
12+
LIBEXT := .so
13+
RPATH := -Wl,-rpath,$(BIN_DIR)
14+
endif
15+
16+
LIBFILE = $(LIB)$(LIBEXT)
17+
18+
.PHONY: run build clean
19+
20+
build:
21+
mkdir -p $(BIN_DIR)
22+
$(MAKE) -C $(ROOT) build-c-lib
23+
cp -f $(LIBFILE) $(BIN_DIR)/
24+
$(CC) -I$(BUILD_DIR) -L$(BIN_DIR) -o $(BIN_DIR)/c-app main.c -lgowalletsdk $(RPATH)
25+
26+
run: build
27+
$(BIN_DIR)/c-app
28+
29+
clean:
30+
rm -rf $(BIN_DIR)

examples/c-app/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# C example using the Go Wallet SDK shared library
2+
3+
Build steps:
4+
- From repo root: make build-c-lib
5+
- Then: cd examples/c-app && make build
6+
7+
Run the example:
8+
```bash
9+
make
10+
cd bin/
11+
./c-app
12+
```
13+
14+
Notes:
15+
- The build generates build/libgowalletsdk.(so|dylib) and header build/libgowalletsdk.h at the repo root.
16+
- On macOS, the example copies the dylib next to the executable and sets rpath for convenience.
17+
- Exported functions:
18+
- GoWSK_NewClient(const char* rpcURL, char** errOut) -> unsigned long long
19+
- GoWSK_CloseClient(unsigned long long handle)
20+
- GoWSK_ChainID(unsigned long long handle, char** errOut) -> char*
21+
- GoWSK_GetBalance(unsigned long long handle, const char* address, char** errOut) -> char*
22+
- GoWSK_FreeCString(char* s)

examples/c-app/main.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
5+
// Header generated by Go build -buildmode=c-shared
6+
#include "libgowalletsdk.h"
7+
8+
int main(int argc, char** argv) {
9+
const char* url = "https://ethereum-rpc.publicnode.com";
10+
const char* addr = "0x0000000000000000000000000000000000000000";
11+
12+
char* err = NULL;
13+
unsigned long long h = GoWSK_NewClient((char*)url, &err);
14+
if (h == 0) {
15+
fprintf(stderr, "Failed to create client: %s\n", err ? err : "unknown error");
16+
if (err) GoWSK_FreeCString(err);
17+
return 1;
18+
}
19+
20+
char* chain = GoWSK_ChainID(h, &err);
21+
if (chain == NULL) {
22+
fprintf(stderr, "ChainID error: %s\n", err ? err : "unknown error");
23+
if (err) GoWSK_FreeCString(err);
24+
GoWSK_CloseClient(h);
25+
return 1;
26+
}
27+
printf("ChainID: %s\n", chain);
28+
GoWSK_FreeCString(chain);
29+
30+
char* balance = GoWSK_GetBalance(h, (char*)addr, &err);
31+
if (balance == NULL) {
32+
fprintf(stderr, "GetBalance error: %s\n", err ? err : "unknown error");
33+
if (err) GoWSK_FreeCString(err);
34+
GoWSK_CloseClient(h);
35+
return 1;
36+
}
37+
printf("Balance(wei): %s\n", balance);
38+
GoWSK_FreeCString(balance);
39+
40+
GoWSK_CloseClient(h);
41+
return 0;
42+
}

0 commit comments

Comments
 (0)