Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/

build/**

examples/c-app/bin/**
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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

131 changes: 131 additions & 0 deletions cshared/lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package main

/*
#include <stdlib.h>
*/
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{}

Choose a reason for hiding this comment

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

I didn't expect to have logic in this layer. Expected to have C bindings to expose wallet SDK functionality to a client. In that case, a client would take care of concurrency, storing ethcleints and so on.

for example:

func GoWSK_NewClient(rpcURL *C.char, errOut **C.char) C.uintptr_t {
	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)
	h := cgo.NewHandle(client)
	return C.uintptr_t(h)
}

func GoWSK_ChainID(handle C.uintptr_t, errOut **C.char) *C.char {
	client := cgo.Handle(handle).Value().(*sdkethclient.Client)
	id, err := client.EthChainId(context.Background())
	if err != nil {
		if errOut != nil {
			*errOut = C.CString(err.Error())
		}
		return nil
	}
	return C.CString(id.String())
}

)

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

we probably want to "namespace" our function names a bit more, call this something like GoWSK_ethclient_NewClient

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() {}
90 changes: 89 additions & 1 deletion docs/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

-
30 changes: 30 additions & 0 deletions examples/c-app/Makefile
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 additions & 0 deletions examples/c-app/README.md
Original file line number Diff line number Diff line change
@@ -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)
42 changes: 42 additions & 0 deletions examples/c-app/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 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;
}