diff --git a/cmd/server.go b/cmd/server.go index 58ff9656..8639cb0c 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -18,21 +18,22 @@ import ( var ( appVersion = "v1.2.0" - chainIDMap = map[string]int{"sepolia": 11155111, "holesky": 17000, "opengradient": 10740} + chainIDMap = map[string]int{"sepolia": 11155111, "holesky": 17000, "opengradient": 10740, "base_sepolia": 84532} httpPortFlag = flag.Int("httpport", 8090, "Listener port to serve HTTP connection") proxyCntFlag = flag.Int("proxycount", 1, "Count of reverse proxies in front of the server") versionFlag = flag.Bool("version", false, "Print version number") - payoutFlag = flag.Float64("faucet.amount", 0.03, "Number of Ethers to transfer per user request") - intervalFlag = flag.Int("faucet.minutes", 300, "Number of minutes to wait between funding rounds") - netnameFlag = flag.String("faucet.name", "opengradient", "Network name to display on the frontend") - symbolFlag = flag.String("faucet.symbol", "OGETH", "Token symbol to display on the frontend") + payoutFlag = flag.Float64("faucet.amount", 0.1, "Number of tokens to transfer per user request") + intervalFlag = flag.Int("faucet.minutes", 300, "Number of minutes to wait between funding rounds") + netnameFlag = flag.String("faucet.name", "base_sepolia", "Network name to display on the frontend") + symbolFlag = flag.String("faucet.symbol", "OPG", "Token symbol to display on the frontend") + tokenAddrFlag = flag.String("faucet.tokenaddr", "0x240b09731D96979f50B2C649C9CE10FcF9C7987F", "ERC-20 token contract address to disperse (empty for native ETH)") keyJSONFlag = flag.String("wallet.keyjson", os.Getenv("KEYSTORE"), "Keystore file to fund user requests with") keyPassFlag = flag.String("wallet.keypass", "password.txt", "Passphrase text file to decrypt keystore") privKeyFlag = flag.String("wallet.privkey", os.Getenv("PRIVATE_KEY"), "Private key hex to fund user requests with") - providerFlag = flag.String("wallet.provider", "https://ogevmdevnet.opengradient.ai", "Endpoint for Ethereum JSON-RPC connection") + providerFlag = flag.String("wallet.provider", "https://sepolia.base.org", "Endpoint for Ethereum JSON-RPC connection") hcaptchaSiteKeyFlag = flag.String("hcaptcha.sitekey", os.Getenv("HCAPTCHA_SITEKEY"), "hCaptcha sitekey") hcaptchaSecretFlag = flag.String("hcaptcha.secret", os.Getenv("HCAPTCHA_SECRET"), "hCaptcha secret") @@ -56,7 +57,7 @@ func Execute() { chainID = big.NewInt(int64(value)) } - txBuilder, err := chain.NewTxBuilder(*providerFlag, privateKey, chainID) + txBuilder, err := chain.NewTxBuilder(*providerFlag, privateKey, chainID, *tokenAddrFlag) if err != nil { panic(fmt.Errorf("cannot connect to web3 provider: %w", err)) } diff --git a/internal/chain/transaction.go b/internal/chain/transaction.go index 34d3f858..4ebb3421 100644 --- a/internal/chain/transaction.go +++ b/internal/chain/transaction.go @@ -27,9 +27,10 @@ type TxBuild struct { fromAddress common.Address nonce uint64 supportsEIP1559 bool + tokenAddress *common.Address } -func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.Int) (TxBuilder, error) { +func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.Int, tokenAddr string) (TxBuilder, error) { client, err := ethclient.Dial(provider) if err != nil { return nil, err @@ -54,6 +55,12 @@ func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.In fromAddress: crypto.PubkeyToAddress(privateKey.PublicKey), supportsEIP1559: supportsEIP1559, } + + if tokenAddr != "" { + addr := common.HexToAddress(tokenAddr) + txBuilder.tokenAddress = &addr + } + txBuilder.refreshNonce(context.Background()) return txBuilder, nil @@ -68,13 +75,24 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm toAddress := common.HexToAddress(to) nonce := b.getAndIncrementNonce() + // Determine transaction target and data based on whether this is an ERC-20 transfer + txTo := &toAddress + txValue := value + var txData []byte + + if b.tokenAddress != nil { + txTo = b.tokenAddress + txValue = big.NewInt(0) + txData = buildERC20TransferData(toAddress, value) + } + var err error var unsignedTx *types.Transaction if b.supportsEIP1559 { - unsignedTx, err = b.buildEIP1559Tx(ctx, &toAddress, value, gasLimit, nonce) + unsignedTx, err = b.buildEIP1559Tx(ctx, txTo, txValue, gasLimit, nonce, txData) } else { - unsignedTx, err = b.buildLegacyTx(ctx, &toAddress, value, gasLimit, nonce) + unsignedTx, err = b.buildLegacyTx(ctx, txTo, txValue, gasLimit, nonce, txData) } if err != nil { @@ -101,7 +119,21 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm return signedTx.Hash(), nil } -func (b *TxBuild) buildEIP1559Tx(ctx context.Context, to *common.Address, value *big.Int, gasLimit uint64, nonce uint64) (*types.Transaction, error) { +// buildERC20TransferData constructs calldata for ERC-20 transfer(address,uint256) +func buildERC20TransferData(to common.Address, amount *big.Int) []byte { + // Function selector: keccak256("transfer(address,uint256)") = 0xa9059cbb + methodID := []byte{0xa9, 0x05, 0x9c, 0xbb} + paddedAddress := common.LeftPadBytes(to.Bytes(), 32) + paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) + + var data []byte + data = append(data, methodID...) + data = append(data, paddedAddress...) + data = append(data, paddedAmount...) + return data +} + +func (b *TxBuild) buildEIP1559Tx(ctx context.Context, to *common.Address, value *big.Int, gasLimit uint64, nonce uint64, data []byte) (*types.Transaction, error) { header, err := b.client.HeaderByNumber(ctx, nil) if err != nil { return nil, err @@ -124,10 +156,11 @@ func (b *TxBuild) buildEIP1559Tx(ctx context.Context, to *common.Address, value Gas: gasLimit, To: to, Value: value, + Data: data, }), nil } -func (b *TxBuild) buildLegacyTx(ctx context.Context, to *common.Address, value *big.Int, gasLimit uint64, nonce uint64) (*types.Transaction, error) { +func (b *TxBuild) buildLegacyTx(ctx context.Context, to *common.Address, value *big.Int, gasLimit uint64, nonce uint64, data []byte) (*types.Transaction, error) { gasPrice, err := b.client.SuggestGasPrice(ctx) if err != nil { return nil, err @@ -142,6 +175,7 @@ func (b *TxBuild) buildLegacyTx(ctx context.Context, to *common.Address, value * Gas: gasLimit, To: to, Value: value, + Data: data, }), nil } diff --git a/web/src/Faucet.svelte b/web/src/Faucet.svelte index 6aaaef2d..d9596c50 100644 --- a/web/src/Faucet.svelte +++ b/web/src/Faucet.svelte @@ -33,7 +33,7 @@ hcaptchaLoaded = true; }; - $: document.title = "OpenGradient Devnet Faucet"; + $: document.title = "$" + faucetInfo.symbol + " Faucet on " + capitalize(faucetInfo.network.replace('_', ' ')); let widgetID; $: if (mounted && hcaptchaLoaded) { @@ -107,8 +107,7 @@ } function capitalize(str) { - const lower = str.toLowerCase(); - return str.charAt(0).toUpperCase() + lower.slice(1); + return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); } @@ -132,7 +131,7 @@ - OpenGradient Devnet Faucet + ${faucetInfo.symbol} Faucet on {capitalize(faucetInfo.network.replace('_', ' '))}