Skip to content

Commit 9c763ee

Browse files
author
corey
committed
add external sign
1 parent 75e2984 commit 9c763ee

File tree

6 files changed

+380
-30
lines changed

6 files changed

+380
-30
lines changed

token-price-oracle/client/l2_client.go

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@ import (
55
"fmt"
66
"math/big"
77

8+
"github.com/morph-l2/externalsign"
89
"github.com/morph-l2/go-ethereum/accounts/abi/bind"
910
"github.com/morph-l2/go-ethereum/common"
1011
"github.com/morph-l2/go-ethereum/crypto"
1112
"github.com/morph-l2/go-ethereum/ethclient"
13+
"github.com/morph-l2/go-ethereum/log"
14+
"morph-l2/token-price-oracle/config"
1215
)
1316

1417
// L2Client wraps L2 chain client
1518
type L2Client struct {
16-
client *ethclient.Client
17-
chainID *big.Int
18-
opts *bind.TransactOpts
19+
client *ethclient.Client
20+
chainID *big.Int
21+
opts *bind.TransactOpts
22+
signer *Signer
23+
externalSign bool
1924
}
2025

2126
// NewL2Client creates new L2 client
22-
func NewL2Client(rpcURL string, privateKey string) (*L2Client, error) {
27+
func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) {
2328
client, err := ethclient.Dial(rpcURL)
2429
if err != nil {
2530
return nil, fmt.Errorf("failed to dial L2 RPC: %w", err)
@@ -38,27 +43,62 @@ func NewL2Client(rpcURL string, privateKey string) (*L2Client, error) {
3843
return nil, fmt.Errorf("failed to get chain ID: %w", err)
3944
}
4045

41-
// Parse private key (remove 0x prefix if present)
42-
privateKeyHex := privateKey
43-
if len(privateKey) > 2 && privateKey[:2] == "0x" {
44-
privateKeyHex = privateKey[2:]
45-
}
46-
key, err := crypto.HexToECDSA(privateKeyHex)
47-
if err != nil {
48-
return nil, fmt.Errorf("failed to parse private key: %w", err)
46+
l2Client := &L2Client{
47+
client: client,
48+
chainID: chainID,
49+
externalSign: cfg.ExternalSign,
4950
}
5051

51-
// Create transaction options
52-
opts, err := bind.NewKeyedTransactorWithChainID(key, chainID)
53-
if err != nil {
54-
return nil, fmt.Errorf("failed to create transactor: %w", err)
52+
if cfg.ExternalSign {
53+
// External sign mode
54+
rsaPriv, err := externalsign.ParseRsaPrivateKey(cfg.ExternalSignRsaPriv)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
57+
}
58+
59+
l2Client.signer = NewSigner(
60+
true,
61+
cfg.ExternalSignAppid,
62+
rsaPriv,
63+
cfg.ExternalSignAddress,
64+
cfg.ExternalSignChain,
65+
cfg.ExternalSignUrl,
66+
chainID,
67+
)
68+
69+
// Create opts with external signer address (for read-only operations)
70+
l2Client.opts = &bind.TransactOpts{
71+
From: common.HexToAddress(cfg.ExternalSignAddress),
72+
NoSend: true, // We'll handle sending manually
73+
}
74+
75+
log.Info("L2 client initialized with external signing",
76+
"address", cfg.ExternalSignAddress,
77+
"chainID", chainID)
78+
} else {
79+
// Local private key mode
80+
privateKeyHex := cfg.PrivateKey
81+
if len(cfg.PrivateKey) > 2 && cfg.PrivateKey[:2] == "0x" {
82+
privateKeyHex = cfg.PrivateKey[2:]
83+
}
84+
key, err := crypto.HexToECDSA(privateKeyHex)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to parse private key: %w", err)
87+
}
88+
89+
// Create transaction options
90+
opts, err := bind.NewKeyedTransactorWithChainID(key, chainID)
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to create transactor: %w", err)
93+
}
94+
l2Client.opts = opts
95+
96+
log.Info("L2 client initialized with local signing",
97+
"address", opts.From.Hex(),
98+
"chainID", chainID)
5599
}
56100

57-
return &L2Client{
58-
client: client,
59-
chainID: chainID,
60-
opts: opts,
61-
}, nil
101+
return l2Client, nil
62102
}
63103

64104
// Close closes client connection
@@ -98,3 +138,18 @@ func (c *L2Client) GetBalance(ctx context.Context, address common.Address) (*big
98138
func (c *L2Client) WalletAddress() common.Address {
99139
return c.opts.From
100140
}
141+
142+
// IsExternalSign returns whether external signing is enabled
143+
func (c *L2Client) IsExternalSign() bool {
144+
return c.externalSign
145+
}
146+
147+
// GetSigner returns the external signer (nil if using local signing)
148+
func (c *L2Client) GetSigner() *Signer {
149+
return c.signer
150+
}
151+
152+
// GetChainID returns the chain ID
153+
func (c *L2Client) GetChainID() *big.Int {
154+
return c.chainID
155+
}

token-price-oracle/client/sign.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"crypto/rsa"
6+
"fmt"
7+
"math/big"
8+
9+
"github.com/morph-l2/externalsign"
10+
"github.com/morph-l2/go-ethereum"
11+
"github.com/morph-l2/go-ethereum/common"
12+
"github.com/morph-l2/go-ethereum/core/types"
13+
"github.com/morph-l2/go-ethereum/log"
14+
)
15+
16+
// Signer handles transaction signing with support for both local and external signing
17+
type Signer struct {
18+
externalSign bool
19+
externalSigner *externalsign.ExternalSign
20+
externalSignUrl string
21+
externalSignAddress common.Address
22+
chainID *big.Int
23+
signer types.Signer
24+
}
25+
26+
// NewSigner creates a new Signer instance
27+
func NewSigner(
28+
externalSign bool,
29+
externalSignAppid string,
30+
externalRsaPriv *rsa.PrivateKey,
31+
externalSignAddress string,
32+
externalSignChain string,
33+
externalSignUrl string,
34+
chainID *big.Int,
35+
) *Signer {
36+
signer := types.NewLondonSigner(chainID)
37+
38+
s := &Signer{
39+
externalSign: externalSign,
40+
externalSignUrl: externalSignUrl,
41+
externalSignAddress: common.HexToAddress(externalSignAddress),
42+
chainID: chainID,
43+
signer: signer,
44+
}
45+
46+
if externalSign {
47+
s.externalSigner = externalsign.NewExternalSign(
48+
externalSignAppid,
49+
externalRsaPriv,
50+
externalSignAddress,
51+
externalSignChain,
52+
signer,
53+
)
54+
log.Info("External signer initialized",
55+
"address", externalSignAddress,
56+
"chain", externalSignChain)
57+
}
58+
59+
return s
60+
}
61+
62+
// Sign signs a transaction using either external or local signing
63+
func (s *Signer) Sign(tx *types.Transaction) (*types.Transaction, error) {
64+
if !s.externalSign {
65+
return nil, fmt.Errorf("local signing not supported in Signer, use bind.TransactOpts")
66+
}
67+
68+
signedTx, err := s.externalSigner.RequestSign(s.externalSignUrl, tx)
69+
if err != nil {
70+
return nil, fmt.Errorf("external sign request failed: %w", err)
71+
}
72+
return signedTx, nil
73+
}
74+
75+
// IsExternalSign returns whether external signing is enabled
76+
func (s *Signer) IsExternalSign() bool {
77+
return s.externalSign
78+
}
79+
80+
// GetFromAddress returns the signer's address
81+
func (s *Signer) GetFromAddress() common.Address {
82+
return s.externalSignAddress
83+
}
84+
85+
// CreateAndSignTx creates a new transaction and signs it
86+
func (s *Signer) CreateAndSignTx(
87+
ctx context.Context,
88+
client *L2Client,
89+
to common.Address,
90+
callData []byte,
91+
) (*types.Transaction, error) {
92+
from := s.externalSignAddress
93+
94+
nonce, err := client.GetClient().NonceAt(ctx, from, nil)
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to get nonce: %w", err)
97+
}
98+
99+
// Get gas tip cap
100+
tip, err := client.GetClient().SuggestGasTipCap(ctx)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to get gas tip cap: %w", err)
103+
}
104+
105+
// Get base fee from latest block
106+
head, err := client.GetClient().HeaderByNumber(ctx, nil)
107+
if err != nil {
108+
return nil, fmt.Errorf("failed to get block header: %w", err)
109+
}
110+
111+
var gasFeeCap *big.Int
112+
if head.BaseFee != nil {
113+
gasFeeCap = new(big.Int).Add(
114+
tip,
115+
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
116+
)
117+
} else {
118+
gasFeeCap = new(big.Int).Set(tip)
119+
}
120+
121+
// Estimate gas
122+
gas, err := client.GetClient().EstimateGas(ctx, ethereum.CallMsg{
123+
From: from,
124+
To: &to,
125+
GasFeeCap: gasFeeCap,
126+
GasTipCap: tip,
127+
Data: callData,
128+
})
129+
if err != nil {
130+
return nil, fmt.Errorf("failed to estimate gas: %w", err)
131+
}
132+
133+
// Add 50% buffer to gas estimate
134+
gas = gas * 3 / 2
135+
136+
// Create transaction
137+
tx := types.NewTx(&types.DynamicFeeTx{
138+
ChainID: s.chainID,
139+
Nonce: nonce,
140+
GasTipCap: tip,
141+
GasFeeCap: gasFeeCap,
142+
Gas: gas,
143+
To: &to,
144+
Data: callData,
145+
})
146+
147+
log.Info("Created transaction for signing",
148+
"from", from.Hex(),
149+
"to", to.Hex(),
150+
"nonce", nonce,
151+
"gas", gas,
152+
"gasFeeCap", gasFeeCap,
153+
"gasTipCap", tip)
154+
155+
// Sign transaction
156+
return s.Sign(tx)
157+
}
158+

token-price-oracle/cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func Main(cliCtx *cli.Context) error {
104104
}
105105

106106
// Create L2 client
107-
l2Client, err := client.NewL2Client(cfg.L2RPC, cfg.PrivateKey)
107+
l2Client, err := client.NewL2Client(cfg.L2RPC, cfg)
108108
if err != nil {
109109
return fmt.Errorf("failed to create L2 client: %w", err)
110110
}

token-price-oracle/config/config.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ type Config struct {
6565
BitgetAPIBaseURL string // Bitget API base URL
6666
BinanceAPIBaseURL string // Binance API base URL
6767

68+
// External sign
69+
ExternalSign bool
70+
ExternalSignAddress string
71+
ExternalSignAppid string
72+
ExternalSignChain string
73+
ExternalSignUrl string
74+
ExternalSignRsaPriv string
75+
6876
// Metrics
6977
MetricsServerEnable bool
7078
MetricsHostname string
@@ -85,6 +93,14 @@ func LoadConfig(ctx *cli.Context) (*Config, error) {
8593
L2RPC: ctx.String(flags.L2EthRPCFlag.Name),
8694
PrivateKey: ctx.String(flags.PrivateKeyFlag.Name),
8795

96+
// External sign
97+
ExternalSign: ctx.Bool(flags.ExternalSignFlag.Name),
98+
ExternalSignAddress: ctx.String(flags.ExternalSignAddressFlag.Name),
99+
ExternalSignAppid: ctx.String(flags.ExternalSignAppidFlag.Name),
100+
ExternalSignChain: ctx.String(flags.ExternalSignChainFlag.Name),
101+
ExternalSignUrl: ctx.String(flags.ExternalSignUrlFlag.Name),
102+
ExternalSignRsaPriv: ctx.String(flags.ExternalSignRsaPrivFlag.Name),
103+
88104
MetricsServerEnable: ctx.Bool(flags.MetricsServerEnableFlag.Name),
89105
MetricsHostname: ctx.String(flags.MetricsHostnameFlag.Name),
90106
MetricsPort: ctx.Uint64(flags.MetricsPortFlag.Name),
@@ -208,6 +224,21 @@ func LoadConfig(ctx *cli.Context) (*Config, error) {
208224
}
209225
}
210226

227+
// Validate external sign config
228+
if cfg.ExternalSign {
229+
if cfg.ExternalSignAddress == "" || cfg.ExternalSignUrl == "" ||
230+
cfg.ExternalSignAppid == "" || cfg.ExternalSignChain == "" ||
231+
cfg.ExternalSignRsaPriv == "" {
232+
return nil, fmt.Errorf("external sign is enabled but missing required config: address=%s, url=%s, appid=%s, chain=%s",
233+
cfg.ExternalSignAddress, cfg.ExternalSignUrl, cfg.ExternalSignAppid, cfg.ExternalSignChain)
234+
}
235+
} else {
236+
// If not using external sign, private key is required
237+
if cfg.PrivateKey == "" {
238+
return nil, fmt.Errorf("private key is required when external sign is not enabled")
239+
}
240+
}
241+
211242
return cfg, nil
212243
}
213244

0 commit comments

Comments
 (0)