代码库地址: https://github.com/SavourDao/savour-hd
Savour HD 是 Savour 项目的钱包的 HD. 后端服务,使用 golang 编写,提供 grpc 接口给上层服务访问
这是一个支持多链的 HD 钱包的服务端代码,包含对接个链的 wallet 模块,rpc f服务端,代码设计是清晰,规整;今天我把他推荐给大家使用。
1. 项目运行
1.1. 安装依赖
go mod tidy
1.2. 构建程序
go build 或者 go install savour-hd
1.3. 启动程序
./savour-hd -c ./config.yml
. 1.4. 启动 RPC 接口测试界面
grpcui -plaintext 127.0.0.1:8089
2. 项目简介
上图是该项目的一个大概的设计图,整个项目包含了 RPC 服务,钱包 factory 和与各个底层链的对接。
func main() {
var f = flag.String("c", "config.yml", "config path")
flag.Parse()
conf, err := config.New(*f)
if err != nil {
panic(err)
}
dispatcher, err := walletdispatcher.New(conf)
if err != nil {
log.Error("Setup dispatcher failed", "err", err)
panic(err)
}
grpcServer := grpc.NewServer(grpc.UnaryInterceptor(dispatcher.Interceptor))
defer grpcServer.GracefulStop()
wallet2.RegisterWalletServiceServer(grpcServer, dispatcher)
listen, err := net.Listen("tcp", ":"+conf.Server.Port)
if err != nil {
log.Error("net listen failed", "err", err)
panic(err)
}
reflection.Register(grpcServer)
log.Info("savour dao start success", "port", conf.Server.Port)
if err := grpcServer.Serve(listen); err != nil {
log.Error("grpc server serve failed", "err", err)
panic(err)
}
}
package walletdispatcher
import (
"context"
"github.com/SavourDao/savour-hd/rpc/common"
"github.com/SavourDao/savour-hd/wallet"
"github.com/SavourDao/savour-hd/wallet/solana"
"runtime/debug"
"strings"
"github.com/SavourDao/savour-hd/config"
wallet2 "github.com/SavourDao/savour-hd/rpc/wallet"
"github.com/SavourDao/savour-hd/wallet/bitcoin"
"github.com/SavourDao/savour-hd/wallet/ethereum"
"github.com/ethereum/go-ethereum/log"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type CommonRequest interface {
GetChain() string
}
type CommonReply = wallet2.SupportCoinsResponse
type ChainType = string
type WalletDispatcher struct {
registry map[ChainType]wallet.WalletAdaptor
}
func (d *WalletDispatcher) mustEmbedUnimplementedWalletServiceServer() {
//TODO implement me
panic("implement me")
}
func New(conf *config.Config) (*WalletDispatcher, error) {
dispatcher := WalletDispatcher{
registry: make(map[ChainType]wallet.WalletAdaptor),
}
walletAdaptorFactoryMap := map[string]func(conf *config.Config) (wallet.WalletAdaptor, error){
bitcoin.ChainName: bitcoin.NewChainAdaptor,
ethereum.ChainName: ethereum.NewChainAdaptor,
solana.ChainName: solana.NewChainAdaptor,
}
supportedChains := []string{bitcoin.ChainName, ethereum.ChainName, solana.ChainName}
for _, c := range conf.Chains {
if factory, ok := walletAdaptorFactoryMap[c]; ok {
adaptor, err := factory(conf)
if err != nil {
log.Crit("failed to setup chain", "chain", c, "error", err)
}
dispatcher.registry[c] = adaptor
} else {
log.Error("unsupported chain", "chain", c, "supportedChains", supportedChains)
}
}
return &dispatcher, nil
}
func NewLocal(network config.NetWorkType) *WalletDispatcher {
dispatcher := WalletDispatcher{
registry: make(map[ChainType]wallet.WalletAdaptor),
}
walletAdaptorFactoryMap := map[string]func(network config.NetWorkType) wallet.WalletAdaptor{
bitcoin.ChainName: bitcoin.NewLocalChainAdaptor,
ethereum.ChainName: ethereum.NewLocalWalletAdaptor,
}
supportedChains := []string{bitcoin.ChainName, ethereum.ChainName}
for _, c := range supportedChains {
if factory, ok := walletAdaptorFactoryMap[c]; ok {
dispatcher.registry[c] = factory(network)
}
}
return &dispatcher
}
func (d *WalletDispatcher) Interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
if e := recover(); e != nil {
log.Error("panic error", "msg", e)
log.Debug(string(debug.Stack()))
err = status.Errorf(codes.Internal, "Panic err: %v", e)
}
}()
pos := strings.LastIndex(info.FullMethod, "/")
method := info.FullMethod[pos+1:]
chain := req.(CommonRequest).GetChain()
log.Info(method, "chain", chain, "req", req)
resp, err = handler(ctx, req)
log.Debug("Finish handling", "resp", resp, "err", err)
return
}
func (d *WalletDispatcher) preHandler(req interface{}) (resp *CommonReply) {
chain := req.(CommonRequest).GetChain()
if _, ok := d.registry[chain]; !ok {
return &CommonReply{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
Support: false,
}
}
return nil
}
func (d *WalletDispatcher) GetSupportCoins(ctx context.Context, request *wallet2.SupportCoinsRequest) (*wallet2.SupportCoinsResponse, error) {
//TODO implement me
panic("implement me")
}
func (d *WalletDispatcher) GetNonce(ctx context.Context, request *wallet2.NonceRequest) (*wallet2.NonceResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.NonceResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
}, nil
}
return d.registry[request.Chain].GetNonce(request)
}
func (d *WalletDispatcher) GetGasPrice(ctx context.Context, request *wallet2.GasPriceRequest) (*wallet2.GasPriceResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.GasPriceResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
Gas: "",
}, nil
}
return d.registry[request.Chain].GetGasPrice(request)
}
func (d *WalletDispatcher) SendTx(ctx context.Context, request *wallet2.SendTxRequest) (*wallet2.SendTxResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.SendTxResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
TxHash: "",
}, nil
}
return d.registry[request.Chain].SendTx(request)
}
func (d *WalletDispatcher) GetBalance(ctx context.Context, request *wallet2.BalanceRequest) (*wallet2.BalanceResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.BalanceResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
Balance: "",
}, nil
}
return d.registry[request.Chain].GetBalance(request)
}
func (d *WalletDispatcher) GetTxByAddress(ctx context.Context, request *wallet2.TxAddressRequest) (*wallet2.TxAddressResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.TxAddressResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
Tx: nil,
}, nil
}
return d.registry[request.Chain].GetTxByAddress(request)
}
func (d *WalletDispatcher) GetTxByHash(ctx context.Context, request *wallet2.TxHashRequest) (*wallet2.TxHashResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.TxHashResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
Tx: nil,
}, nil
}
return d.registry[request.Chain].GetTxByHash(request)
}
func (d *WalletDispatcher) GetAccount(ctx context.Context, request *wallet2.AccountRequest) (*wallet2.AccountResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.AccountResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
AccountNumber: "",
Sequence: "",
}, nil
}
return d.registry[request.Chain].GetAccount(request)
}
func (d *WalletDispatcher) GetUtxo(ctx context.Context, request *wallet2.UtxoRequest) (*wallet2.UtxoResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.UtxoResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
}, nil
}
return d.registry[request.Chain].GetUtxo(request)
}
func (d *WalletDispatcher) GetMinRent(ctx context.Context, request *wallet2.MinRentRequest) (*wallet2.MinRentResponse, error) {
resp := d.preHandler(request)
if resp != nil {
return &wallet2.MinRentResponse{
Code: common.ReturnCode_ERROR,
Msg: config.UnsupportedOperation,
Value: "",
}, nil
}
return d.registry[request.Chain].GetMinRent(request)
}
具体的详情大家可以看项目源码。