From 0a6d9b48ab03ac8ea4b74b049023bbd307d7abbb Mon Sep 17 00:00:00 2001 From: Wisdom Arerosuoghene Date: Wed, 8 Nov 2023 08:46:18 +0100 Subject: [PATCH] initial cgo code --- .gitignore | 1 + cgo/addresses.go | 26 ++++++++ cgo/cgo.go | 82 ++++++++++++++++++++++++ cgo/log.go | 26 ++++++++ cgo/utils.go | 34 ++++++++++ cgo/walletloader.go | 147 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 316 insertions(+) create mode 100644 .gitignore create mode 100644 cgo/addresses.go create mode 100644 cgo/cgo.go create mode 100644 cgo/log.go create mode 100644 cgo/utils.go create mode 100644 cgo/walletloader.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/cgo/addresses.go b/cgo/addresses.go new file mode 100644 index 0000000..9ca7400 --- /dev/null +++ b/cgo/addresses.go @@ -0,0 +1,26 @@ +package main + +import "C" +import "decred.org/dcrwallet/v3/wallet/udb" + +//export currentReceiveAddress +func currentReceiveAddress(walletName *C.char) *C.char { + w, ok := loadedWallet(walletName) + if !ok { + return nil + } + + // Don't return an address if not synced! + if !w.IsSynced() { + w.log.Trace("currentReceiveAddress requested on an unsynced wallet") + return nil + } + + addr, err := w.CurrentAddress(udb.DefaultAccountNum) + if err != nil { + w.log.Errorf("w.CurrentAddress error: %v", err) + return nil + } + + return cString(addr.String()) +} diff --git a/cgo/cgo.go b/cgo/cgo.go new file mode 100644 index 0000000..12df30f --- /dev/null +++ b/cgo/cgo.go @@ -0,0 +1,82 @@ +// A pacakge that exports Decred wallet functionalities as go code that can be +// compiled into a c-shared libary. Must be a main package, with an empty main +// function. And functions to be exported must have an "//export {fnName}" +// comment. +// +// Build cmd: go build -buildmode=c-archive -o {path_to_generated_library} ./cgo +// E.g. go build -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo. + +package main + +import "C" +import ( + "context" + "sync" + + "github.com/decred/slog" + "github.com/itswisdomagain/libwallet/asset/dcr" + "github.com/itswisdomagain/libwallet/assetlog" +) + +var ( + ctx context.Context + cancelCtx context.CancelFunc + wg sync.WaitGroup + + logBackend *parentLogger + log slog.Logger + + walletsMtx sync.RWMutex + wallets map[string]*wallet +) + +//export initialize +func initialize(cLogDir *C.char) *C.char { + walletsMtx.Lock() + defer walletsMtx.Unlock() + if wallets != nil { + return cString("duplicate initialization") + } + + logDir := goString(cLogDir) + logSpinner, err := assetlog.NewRotator(logDir, "dcrwallet.log") + if err != nil { + return cStringF("error initializing log rotator: %v", err) + } + + logBackend = newParentLogger(logSpinner) + err = dcr.InitGlobalLogging(logDir, logBackend) + if err != nil { + return cStringF("error initializing logger for external pkgs: %v", err) + } + + log = logBackend.SubLogger("[APP]") + log.SetLevel(slog.LevelTrace) + + ctx, cancelCtx = context.WithCancel(context.Background()) + wallets = make(map[string]*wallet) + + log.Info("libwallet cgo initialized") + return nil +} + +//export shutdown +func shutdown() { + walletsMtx.Lock() + for _, wallet := range wallets { + if err := wallet.CloseWallet(); err != nil { + wallet.log.Errorf("close wallet error: %v", err) + } + } + wallets = nil // cannot be reused unless initialize is called again. + walletsMtx.Unlock() + + // Stop all remaining background processes and wait for them to stop. + cancelCtx() + wg.Wait() + + // Close the logger backend as the last step. + logBackend.Close() +} + +func main() {} diff --git a/cgo/log.go b/cgo/log.go new file mode 100644 index 0000000..9f46b96 --- /dev/null +++ b/cgo/log.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/decred/slog" + "github.com/jrick/logrotate/rotator" +) + +type parentLogger struct { + *slog.Backend + rotator *rotator.Rotator +} + +func newParentLogger(rotator *rotator.Rotator) *parentLogger { + return &parentLogger{ + Backend: slog.NewBackend(rotator), + rotator: rotator, + } +} + +func (pl *parentLogger) SubLogger(name string) slog.Logger { + return pl.Logger(name) +} + +func (pl *parentLogger) Close() error { + return pl.rotator.Close() +} diff --git a/cgo/utils.go b/cgo/utils.go new file mode 100644 index 0000000..087a161 --- /dev/null +++ b/cgo/utils.go @@ -0,0 +1,34 @@ +package main + +import "C" +import ( + "fmt" +) + +func loadedWallet(cName *C.char) (*wallet, bool) { + walletsMtx.Lock() + defer walletsMtx.Unlock() + + name := goString(cName) + w, ok := wallets[name] + if !ok { + log.Debugf("attempted to use an unloaded wallet %q", name) + } + return w, ok +} + +func cString(str string) *C.char { + return C.CString(str) +} + +func cStringF(format string, a ...any) *C.char { + return C.CString(fmt.Sprintf(format, a...)) +} + +func cError(err error) *C.char { + return C.CString(err.Error()) +} + +func goString(cstr *C.char) string { + return C.GoString(cstr) +} diff --git a/cgo/walletloader.go b/cgo/walletloader.go new file mode 100644 index 0000000..9a38e76 --- /dev/null +++ b/cgo/walletloader.go @@ -0,0 +1,147 @@ +package main + +import "C" +import ( + "encoding/json" + + "github.com/decred/slog" + "github.com/itswisdomagain/libwallet/asset" + "github.com/itswisdomagain/libwallet/asset/dcr" +) + +const emptyJsonObject = "{}" + +type wallet struct { + *dcr.Wallet + log slog.Logger +} + +//export createWallet +func createWallet(cName, cDataDir, cNet, cPass *C.char) *C.char { + walletsMtx.Lock() + defer walletsMtx.Unlock() + if wallets == nil { + return cString("libwallet is not initialized") + } + + name := goString(cName) + if _, exists := wallets[name]; exists { + return cStringF("wallet already exists with name: %q", name) + } + + network, err := asset.NetFromString(goString(cNet)) + if err != nil { + return cError(err) + } + + logger := logBackend.Logger("[" + name + "]") + logger.SetLevel(slog.LevelTrace) + params := asset.CreateWalletParams{ + OpenWalletParams: asset.OpenWalletParams{ + Net: network, + DataDir: goString(cDataDir), + DbDriver: "bdb", // use badgerdb for mobile! + Logger: logger, + }, + Pass: []byte(goString(cPass)), + } + w, err := dcr.CreateWallet(ctx, params, nil) + if err != nil { + return cError(err) + } + + wallets[name] = &wallet{ + Wallet: w, + log: logger, + } + return nil +} + +//export loadWallet +func loadWallet(cName, cDataDir, cNet *C.char) *C.char { + walletsMtx.Lock() + defer walletsMtx.Unlock() + if wallets == nil { + return cString("libwallet is not initialized") + } + + name := goString(cName) + if _, exists := wallets[name]; exists { + return nil // not an error, already loaded + } + + network, err := asset.NetFromString(goString(cNet)) + if err != nil { + return cError(err) + } + + logger := logBackend.Logger("[" + name + "]") + logger.SetLevel(slog.LevelTrace) + params := asset.OpenWalletParams{ + Net: network, + DataDir: goString(cDataDir), + DbDriver: "bdb", // use badgerdb for mobile! + Logger: logger, + } + w, err := dcr.LoadWallet(ctx, params) + if err != nil { + return cError(err) + } + + if err = w.OpenWallet(ctx); err != nil { + return cError(err) + } + + wallets[name] = &wallet{ + Wallet: w, + log: logger, + } + return nil +} + +//export walletSeed +func walletSeed(name, pass *C.char) *C.char { + w, ok := loadedWallet(name) + if !ok { + return nil + } + + seed, err := w.DecryptSeed([]byte(goString(pass))) + if err != nil { + w.log.Errorf("w.RevealSeed error: %v", err) + return nil + } + + return cString(seed) +} + +//export walletBalance +func walletBalance(name *C.char) *C.char { + w, ok := loadedWallet(name) + if !ok { + return cString(emptyJsonObject) + } + + balMap := map[string]float64{ + "confirmed": 0, + "unconfirmed": 0, + } + + bals, err := w.AccountBalances(ctx, 0) + if err != nil { + w.log.Errorf("w.AccountBalances error: %v", err) + } else { + for _, bal := range bals { + balMap["confirmed"] += bal.Spendable.ToCoin() + balMap["unconfirmed"] += bal.Total.ToCoin() - bal.Spendable.ToCoin() + } + } + + balJson, err := json.Marshal(balMap) + if err != nil { + w.log.Errorf("marshal balMap error: %v", err) + return cString(emptyJsonObject) + } + + return cString(string(balJson)) +}