Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
108 changes: 87 additions & 21 deletions token-price-oracle/client/l2_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import (
"fmt"
"math/big"

"github.com/morph-l2/externalsign"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"github.com/morph-l2/go-ethereum/accounts/abi/bind"
"github.com/morph-l2/go-ethereum/common"
"github.com/morph-l2/go-ethereum/core/types"
"github.com/morph-l2/go-ethereum/crypto"
"github.com/morph-l2/go-ethereum/ethclient"
"github.com/morph-l2/go-ethereum/log"
"morph-l2/token-price-oracle/config"
)

// L2Client wraps L2 chain client
type L2Client struct {
client *ethclient.Client
chainID *big.Int
opts *bind.TransactOpts
client *ethclient.Client
chainID *big.Int
opts *bind.TransactOpts
signer *Signer
externalSign bool
}

// NewL2Client creates new L2 client
func NewL2Client(rpcURL string, privateKey string) (*L2Client, error) {
func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) {
client, err := ethclient.Dial(rpcURL)
if err != nil {
return nil, fmt.Errorf("failed to dial L2 RPC: %w", err)
Expand All @@ -38,27 +44,72 @@ func NewL2Client(rpcURL string, privateKey string) (*L2Client, error) {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}

// Parse private key (remove 0x prefix if present)
privateKeyHex := privateKey
if len(privateKey) > 2 && privateKey[:2] == "0x" {
privateKeyHex = privateKey[2:]
}
key, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
l2Client := &L2Client{
client: client,
chainID: chainID,
externalSign: cfg.ExternalSign,
}

// Create transaction options
opts, err := bind.NewKeyedTransactorWithChainID(key, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
if cfg.ExternalSign {
// External sign mode
rsaPriv, err := externalsign.ParseRsaPrivateKey(cfg.ExternalSignRsaPriv)
if err != nil {
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
}

l2Client.signer = NewSigner(
true,
cfg.ExternalSignAppid,
rsaPriv,
cfg.ExternalSignAddress,
cfg.ExternalSignChain,
cfg.ExternalSignUrl,
chainID,
)

fromAddr := common.HexToAddress(cfg.ExternalSignAddress)
ethSigner := types.NewLondonSigner(chainID)

// Create opts with a placeholder signer for building transactions
// The actual signing will be done by external signer
l2Client.opts = &bind.TransactOpts{
From: fromAddr,
NoSend: true, // We'll handle sending manually
Signer: func(address common.Address, tx *types.Transaction) (*types.Transaction, error) {
// This is a placeholder signer - we don't actually sign here
// The transaction will be signed by external signer later
// We need to return the transaction with correct signer hash for gas estimation
return tx.WithSignature(ethSigner, make([]byte, 65))
},
Comment thread
curryxbo marked this conversation as resolved.
}

log.Info("L2 client initialized with external signing",
"address", cfg.ExternalSignAddress,
"chainID", chainID)
Comment thread
curryxbo marked this conversation as resolved.
} else {
// Local private key mode
privateKeyHex := cfg.PrivateKey
if len(cfg.PrivateKey) > 2 && cfg.PrivateKey[:2] == "0x" {
privateKeyHex = cfg.PrivateKey[2:]
}
key, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}

// Create transaction options
opts, err := bind.NewKeyedTransactorWithChainID(key, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
}
l2Client.opts = opts

log.Info("L2 client initialized with local signing",
"address", opts.From.Hex(),
"chainID", chainID)
}

return &L2Client{
client: client,
chainID: chainID,
opts: opts,
}, nil
return l2Client, nil
}

// Close closes client connection
Expand Down Expand Up @@ -98,3 +149,18 @@ func (c *L2Client) GetBalance(ctx context.Context, address common.Address) (*big
func (c *L2Client) WalletAddress() common.Address {
return c.opts.From
}

// IsExternalSign returns whether external signing is enabled
func (c *L2Client) IsExternalSign() bool {
return c.externalSign
}

// GetSigner returns the external signer (nil if using local signing)
func (c *L2Client) GetSigner() *Signer {
return c.signer
}

// GetChainID returns the chain ID
func (c *L2Client) GetChainID() *big.Int {
return c.chainID
}
158 changes: 158 additions & 0 deletions token-price-oracle/client/sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package client

import (
"context"
"crypto/rsa"
"fmt"
"math/big"

"github.com/morph-l2/externalsign"
"github.com/morph-l2/go-ethereum"
"github.com/morph-l2/go-ethereum/common"
"github.com/morph-l2/go-ethereum/core/types"
"github.com/morph-l2/go-ethereum/log"
)

// Signer handles transaction signing with support for both local and external signing
type Signer struct {
externalSign bool
externalSigner *externalsign.ExternalSign
externalSignUrl string
externalSignAddress common.Address
chainID *big.Int
signer types.Signer
}

// NewSigner creates a new Signer instance
func NewSigner(
externalSign bool,
externalSignAppid string,
externalRsaPriv *rsa.PrivateKey,
externalSignAddress string,
externalSignChain string,
externalSignUrl string,
chainID *big.Int,
) *Signer {
signer := types.NewLondonSigner(chainID)

s := &Signer{
externalSign: externalSign,
externalSignUrl: externalSignUrl,
externalSignAddress: common.HexToAddress(externalSignAddress),
chainID: chainID,
signer: signer,
}

if externalSign {
s.externalSigner = externalsign.NewExternalSign(
externalSignAppid,
externalRsaPriv,
externalSignAddress,
externalSignChain,
signer,
)
log.Info("External signer initialized",
"address", externalSignAddress,
"chain", externalSignChain)
}

return s
}

// Sign signs a transaction using either external or local signing
func (s *Signer) Sign(tx *types.Transaction) (*types.Transaction, error) {
if !s.externalSign {
return nil, fmt.Errorf("local signing not supported in Signer, use bind.TransactOpts")
}

signedTx, err := s.externalSigner.RequestSign(s.externalSignUrl, tx)
if err != nil {
return nil, fmt.Errorf("external sign request failed: %w", err)
}
return signedTx, nil
}

// IsExternalSign returns whether external signing is enabled
func (s *Signer) IsExternalSign() bool {
return s.externalSign
}

// GetFromAddress returns the signer's address
func (s *Signer) GetFromAddress() common.Address {
return s.externalSignAddress
}

// CreateAndSignTx creates a new transaction and signs it
func (s *Signer) CreateAndSignTx(
ctx context.Context,
client *L2Client,
to common.Address,
callData []byte,
) (*types.Transaction, error) {
from := s.externalSignAddress

nonce, err := client.GetClient().NonceAt(ctx, from, nil)
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}

// Get gas tip cap
tip, err := client.GetClient().SuggestGasTipCap(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get gas tip cap: %w", err)
}

// Get base fee from latest block
head, err := client.GetClient().HeaderByNumber(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to get block header: %w", err)
}

var gasFeeCap *big.Int
if head.BaseFee != nil {
gasFeeCap = new(big.Int).Add(
tip,
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
)
} else {
gasFeeCap = new(big.Int).Set(tip)
}

// Estimate gas
gas, err := client.GetClient().EstimateGas(ctx, ethereum.CallMsg{
From: from,
To: &to,
GasFeeCap: gasFeeCap,
GasTipCap: tip,
Data: callData,
})
if err != nil {
return nil, fmt.Errorf("failed to estimate gas: %w", err)
}

// Add 50% buffer to gas estimate
gas = gas * 3 / 2

// Create transaction
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: s.chainID,
Nonce: nonce,
GasTipCap: tip,
GasFeeCap: gasFeeCap,
Gas: gas,
To: &to,
Data: callData,
})

log.Info("Created transaction for signing",
"from", from.Hex(),
"to", to.Hex(),
"nonce", nonce,
"gas", gas,
"gasFeeCap", gasFeeCap,
"gasTipCap", tip)

// Sign transaction
return s.Sign(tx)
}

2 changes: 1 addition & 1 deletion token-price-oracle/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func Main(cliCtx *cli.Context) error {
}

// Create L2 client
l2Client, err := client.NewL2Client(cfg.L2RPC, cfg.PrivateKey)
l2Client, err := client.NewL2Client(cfg.L2RPC, cfg)
if err != nil {
return fmt.Errorf("failed to create L2 client: %w", err)
}
Expand Down
46 changes: 46 additions & 0 deletions token-price-oracle/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ type Config struct {
BitgetAPIBaseURL string // Bitget API base URL
BinanceAPIBaseURL string // Binance API base URL

// External sign
ExternalSign bool
ExternalSignAddress string
ExternalSignAppid string
ExternalSignChain string
ExternalSignUrl string
ExternalSignRsaPriv string

// Metrics
MetricsServerEnable bool
MetricsHostname string
Expand All @@ -85,6 +93,14 @@ func LoadConfig(ctx *cli.Context) (*Config, error) {
L2RPC: ctx.String(flags.L2EthRPCFlag.Name),
PrivateKey: ctx.String(flags.PrivateKeyFlag.Name),

// External sign
ExternalSign: ctx.Bool(flags.ExternalSignFlag.Name),
ExternalSignAddress: ctx.String(flags.ExternalSignAddressFlag.Name),
ExternalSignAppid: ctx.String(flags.ExternalSignAppidFlag.Name),
ExternalSignChain: ctx.String(flags.ExternalSignChainFlag.Name),
ExternalSignUrl: ctx.String(flags.ExternalSignUrlFlag.Name),
ExternalSignRsaPriv: ctx.String(flags.ExternalSignRsaPrivFlag.Name),

MetricsServerEnable: ctx.Bool(flags.MetricsServerEnableFlag.Name),
MetricsHostname: ctx.String(flags.MetricsHostnameFlag.Name),
MetricsPort: ctx.Uint64(flags.MetricsPortFlag.Name),
Expand Down Expand Up @@ -208,6 +224,36 @@ func LoadConfig(ctx *cli.Context) (*Config, error) {
}
}

// Validate external sign config
if cfg.ExternalSign {
if cfg.ExternalSignAddress == "" || cfg.ExternalSignUrl == "" ||
cfg.ExternalSignAppid == "" || cfg.ExternalSignChain == "" ||
cfg.ExternalSignRsaPriv == "" {
return nil, fmt.Errorf("external sign is enabled but missing required config: address=%s, url=%s, appid=%s, chain=%s",
cfg.ExternalSignAddress, cfg.ExternalSignUrl, cfg.ExternalSignAppid, cfg.ExternalSignChain)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Validate address format
if !common.IsHexAddress(cfg.ExternalSignAddress) {
return nil, fmt.Errorf("invalid external sign address format: %s", cfg.ExternalSignAddress)
}

// Validate URL format
if !strings.HasPrefix(cfg.ExternalSignUrl, "http://") && !strings.HasPrefix(cfg.ExternalSignUrl, "https://") {
return nil, fmt.Errorf("invalid external sign URL format (must start with http:// or https://): %s", cfg.ExternalSignUrl)
}

// Validate RSA private key format
if !strings.Contains(cfg.ExternalSignRsaPriv, "RSA PRIVATE KEY") {
return nil, fmt.Errorf("invalid RSA private key format (must contain 'RSA PRIVATE KEY')")
}
} else {
// If not using external sign, private key is required
if cfg.PrivateKey == "" {
return nil, fmt.Errorf("private key is required when external sign is not enabled")
}
}

return cfg, nil
}

Expand Down
Loading