Skip to content
Snippets Groups Projects
Unverified Commit a15f2729 authored by Ching-Hua (Vivian) Lin's avatar Ching-Hua (Vivian) Lin Committed by GitHub
Browse files

feat: Implement faucet dApp (#531)

* Feat: Integrate faucet page to SPA plugin

* Feat: Add Link to get txn of issued address

* Feat: Add faucet payload

* Feat: Add faucet plugin and minor tweaks

* Fix: Check payload type after the txn is solid

* Fix: Update package name

* Fix: Fix payload test

* Test: Add faucet test

* Refactor: Minor tweaks in plugin/faucet.go

* Feat: Add faucet webapi

* Feat: Apply faucet plugins

* Feat: Apply messagefactory and update to latest develop

* Fix: Fix import error

* Fix: Fix tests in binary/faucet

* Feat: Integrate faucet page to SPA plugin

* Feat: Add Link to get txn of issued address

* Feat: Add faucet payload

* Feat: Add faucet plugin and minor tweaks

* Fix: Check payload type after the txn is solid

* Fix: Update package name

* Fix: Fix payload test

* Test: Add faucet test

* Refactor: Minor tweaks in plugin/faucet.go

* Feat: Add faucet webapi

* Feat: Apply faucet plugins

* Feat: Apply messagefactory and update to latest develop

* Fix: Fix import error

* Fix: Fix tests in binary/faucet

* refactor: Update to latest usage

* fix: Update go.mod, go.sum

* refactor: Disable faucet plugin by default

* fix: Update to latest changes

* feat: Add faucet payload layout

* refactor: Move faucet to dapps

* feat: Enable the faucet to send funds :sparkles:

* Fix: fix tests

* fix: Fix test

* fix: Initiate LedgerState

* Update packr on dashboard

* refactor: refactor SendFunds in faucet dapp

* feat: Add faucet integration test

* feat: Add faucet integration test to script

* doc: Add function descriptions

* fix: fix wrong parameter in CheckBalances

* fix: fix :dog:

* fix some stuff

* make the faucet configurable via CLI flags

* make the faucet seed a parameter in the integration tests

* activate the faucet on the peer master in docker-network

* fixes wrong address route in faucet view

* improves faucet processing log message

* fix log messages in faucet dapp

* improve error message further

* fixes unit tests

* adds tool to auto. gen. address

* dog

* wait for faucet funding tx to get booked before processing the next request

* make the dog stay silent

* decrease pow difficulty to 1 in int. tests

* use 4 as the pow difficulty in integration tests

* :dog:



* fix integration tests

* dec. pow diff to 1

* use 0 for the faucet integration test

* use a worker pool to serve faucet funding requests

Co-authored-by: default avatarLuca Moser <moser.luca@gmail.com>
parent 9696c899
Branches
Tags
No related merge requests found
Showing
with 453 additions and 35 deletions
import * as React from 'react';
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import NodeStore from "app/stores/NodeStore";
import {inject, observer} from "mobx-react";
import {FaucetAddressInput} from "app/components/FaucetAddressInput";
interface Props {
nodeStore?: NodeStore;
}
@inject("nodeStore")
@observer
export class Faucet extends React.Component<Props, any> {
render() {
return (
<Container>
<h3>GoShimmer Faucet</h3>
<Row className={"mb-3"}>
<Col>
<p>
Get tokens from the GoShimmer faucet!
</p>
</Col>
</Row>
<FaucetAddressInput/>
</Container>
);
}
}
import * as React from 'react';
import {KeyboardEvent} from 'react';
import NodeStore from "app/stores/NodeStore";
import FaucetStore from "app/stores/FaucetStore";
import {inject, observer} from "mobx-react";
import FormControl from "react-bootstrap/FormControl";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import Button from 'react-bootstrap/Button'
import InputGroup from "react-bootstrap/InputGroup";
import {Link} from 'react-router-dom';
interface Props {
nodeStore?: NodeStore;
faucetStore?: FaucetStore;
}
@inject("nodeStore")
@inject("faucetStore")
@observer
export class FaucetAddressInput extends React.Component<Props, any> {
updateSend = (e) => {
this.props.faucetStore.updateSend(e.target.value);
};
executeSend = (e: KeyboardEvent) => {
if (e.key !== 'Enter') return;
this.props.faucetStore.sendReq();
};
btnExecuteSend = () => {
this.props.faucetStore.sendReq();
};
render() {
let {send_addr, sending} = this.props.faucetStore;
return (
<React.Fragment>
<Row className={"mb-3"}>
<Col>
<InputGroup className="mb-3">
<FormControl
placeholder="Address"
aria-label="Address"
aria-describedby="basic-addon1"
value={send_addr} onChange={this.updateSend}
onKeyUp={this.executeSend}
disabled={sending}
/>
</InputGroup>
</Col>
</Row>
<Row className={"mb-3"}>
<Col>
<Button
variant="primary"
size="sm" block
onClick={this.btnExecuteSend}
value={send_addr}
disabled={sending}>
Send
</Button>
</Col>
</Row>
<Row className={"mb-3"}>
<Col>
<small>
Check your funds on the explorer: <Link
to={`/explorer/address/${send_addr}`}>{send_addr}</Link>
</small>
</Col>
</Row>
</React.Fragment>
);
}
}
......@@ -13,6 +13,7 @@ import {LinkContainer} from 'react-router-bootstrap';
import {ExplorerMessageQueryResult} from "app/components/ExplorerMessageQueryResult";
import {ExplorerAddressQueryResult} from "app/components/ExplorerAddressResult";
import {Explorer404} from "app/components/Explorer404";
import {Faucet} from "app/components/Faucet";
import {Neighbors} from "app/components/Neighbors";
import {Visualizer} from "app/components/Visualizer";
......@@ -59,6 +60,11 @@ export class Root extends React.Component<Props, any> {
Visualizer
</Nav.Link>
</LinkContainer>
<LinkContainer to="/faucet">
<Nav.Link>
Faucet
</Nav.Link>
</LinkContainer>
</Nav>
<Navbar.Collapse className="justify-content-end">
<NavExplorerSearchbar/>
......@@ -77,6 +83,7 @@ export class Root extends React.Component<Props, any> {
<Route exact path="/explorer/404/:search" component={Explorer404}/>
<Route exact path="/explorer" component={Explorer}/>
<Route exact path="/visualizer" component={Visualizer}/>
<Route exact path="/faucet" component={Faucet}/>
<Redirect to="/dashboard"/>
</Switch>
{this.props.children}
......
export enum PayloadType {
Data = 0,
Value = 1,
Faucet = 2,
Drng = 111,
}
......@@ -12,7 +13,7 @@ export enum DrngSubtype {
// BasicPayload
export class BasicPayload {
content_title: string;
bytes: string;
content: string;
}
// DrngPayload
......
......@@ -167,6 +167,7 @@ export class ExplorerStore {
case PayloadType.Value:
this.payload = msg.payload as ValuePayload
case PayloadType.Data:
case PayloadType.Faucet:
default:
this.payload = msg.payload as BasicPayload
break;
......
import {action, observable} from 'mobx';
class SendResult {
MsgId: string;
}
enum QueryError {
NotFound
}
export class FaucetStore {
// send request to faucet
@observable send_addr: string = "";
@observable sending: boolean = false;
@observable sendResult: SendResult = null;
constructor() {
}
sendReq = async () => {
this.updateSending(true);
try {
// send request
let res = await fetch(`/api/faucet/${this.send_addr}`);
if (res.status !== 200) {
this.updateQueryError(QueryError.NotFound);
return;
}
let result: SendResult = await res.json();
this.updateSendResult(result);
} catch (err) {
this.updateQueryError(err);
}
};
@action
updateSendResult = (result: SendResult) => {
this.sending = false;
this.sendResult = result;
};
@action
resetSend = () => {
this.sending = false;
};
@action
updateSend = (send_addr: string) => {
this.send_addr = send_addr;
};
@action
updateSending = (sending: boolean) => this.sending = sending;
@action
reset = () => {
this.send_addr = null;
};
@action
updateQueryError = (err: any) => {
this.sending = false;
};
}
export default FaucetStore;
......@@ -9,6 +9,7 @@ import {Router} from 'react-router-dom';
import NodeStore from "app/stores/NodeStore";
import ExplorerStore from "app/stores/ExplorerStore";
import DrngStore from "app/stores/DrngStore";
import FaucetStore from "app/stores/FaucetStore";
import VisualizerStore from "app/stores/VisualizerStore";
// prepare MobX stores
......@@ -16,12 +17,14 @@ const routerStore = new RouterStore();
const nodeStore = new NodeStore();
const explorerStore = new ExplorerStore(routerStore);
const drngStore = new DrngStore(routerStore);
const faucetStore = new FaucetStore();
const visualizerStore = new VisualizerStore(routerStore);
const stores = {
"routerStore": routerStore,
"nodeStore": nodeStore,
"explorerStore": explorerStore,
"drngStore": drngStore,
"faucetStore": faucetStore,
"visualizerStore": visualizerStore,
};
......
This diff is collapsed.
package dashboard
import (
faucetpayload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
......@@ -15,7 +16,13 @@ import (
// It can be reused with different payload that only contains one field.
type BasicPayload struct {
ContentTitle string `json:"content_title"`
Bytes []byte `json:"bytes"`
Content []byte `json:"content"`
}
// BasicStringPayload contains content title and string content
type BasicStringPayload struct {
ContentTitle string `json:"content_title"`
Content string `json:"content"`
}
// DrngPayload contains the subtype of drng payload, instance Id
......@@ -70,7 +77,13 @@ func ProcessPayload(p payload.Payload) interface{} {
// data payload
return BasicPayload{
ContentTitle: "Data",
Bytes: p.(*payload.Data).Data(),
Content: p.(*payload.Data).Data(),
}
case faucetpayload.Type:
// faucet payload
return BasicStringPayload{
ContentTitle: "address",
Content: p.(*faucetpayload.Payload).Address().String(),
}
case drngpayload.Type:
// drng payload
......@@ -81,7 +94,7 @@ func ProcessPayload(p payload.Payload) interface{} {
// unknown payload
return BasicPayload{
ContentTitle: "Bytes",
Bytes: p.Bytes(),
Content: p.Bytes(),
}
}
}
......@@ -106,7 +119,7 @@ func processDrngPayload(p payload.Payload) (dp DrngPayload) {
default:
subpayload = BasicPayload{
ContentTitle: "bytes",
Bytes: drngPayload.Bytes(),
Content: drngPayload.Bytes(),
}
}
return DrngPayload{
......
......@@ -71,6 +71,7 @@ func setupRoutes(e *echo.Echo) {
apiRoutes := e.Group("/api")
setupExplorerRoutes(apiRoutes)
setupFaucetRoutes(apiRoutes)
e.HTTPErrorHandler = func(err error, c echo.Context) {
c.Logger().Error(err)
......
package faucet
import (
"net/http"
goSync "sync"
faucetpayload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
"github.com/iotaledger/goshimmer/plugins/messagelayer"
"github.com/iotaledger/goshimmer/plugins/webapi"
"github.com/iotaledger/hive.go/logger"
"github.com/iotaledger/hive.go/node"
"github.com/labstack/echo"
)
const (
// PluginName is the name of the web API faucet endpoint plugin.
PluginName = "WebAPI faucet Endpoint"
)
var (
// plugin is the plugin instance of the web API info endpoint plugin.
plugin *node.Plugin
once goSync.Once
log *logger.Logger
)
// Plugin gets the plugin instance.
func Plugin() *node.Plugin {
once.Do(func() {
plugin = node.NewPlugin(PluginName, node.Enabled, configure)
})
return plugin
}
func configure(plugin *node.Plugin) {
log = logger.NewLogger("API-faucet")
webapi.Server().POST("faucet", requestFunds)
}
// requestFunds creates a faucet request (0-value) message with the given destination address and
// broadcasts it to the node's neighbors. It returns the message ID if successful.
func requestFunds(c echo.Context) error {
var request Request
var addr address.Address
if err := c.Bind(&request); err != nil {
log.Info(err.Error())
return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
}
log.Debug("Received - address:", request.Address)
addr, err := address.FromBase58(request.Address)
if err != nil {
return c.JSON(http.StatusBadRequest, Response{Error: "Invalid address"})
}
// build faucet message with transaction factory
msg := messagelayer.MessageFactory().IssuePayload(faucetpayload.New(addr))
if msg == nil {
return c.JSON(http.StatusInternalServerError, Response{Error: "Fail to send faucetrequest"})
}
return c.JSON(http.StatusOK, Response{ID: msg.Id().String()})
}
// Response contains the ID of the message sent.
type Response struct {
ID string `json:"id,omitempty"`
Error string `json:"error,omitempty"`
}
// Request contains the address to request funds from faucet.
type Request struct {
Address string `json:"address"`
}
......@@ -46,7 +46,8 @@ services:
command: >
--config-dir=/tmp
--database.directory=/tmp/mainnetdb
--node.enablePlugins=bootstrap,prometheus,spammer
--node.enablePlugins=bootstrap,prometheus,spammer,faucet
--faucet.seed=7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih
--valueLayer.snapshot.file=/tmp/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin
volumes:
- ./config.docker.json:/tmp/config.json:ro
......
#!/bin/bash
TEST_NAMES='autopeering common drng message value consensus'
TEST_NAMES='autopeering common drng message value consensus faucet'
echo "Build GoShimmer image"
docker build -t iotaledger/goshimmer ../../.
......
......@@ -85,6 +85,7 @@ func (d *DockerContainer) CreateGoShimmerPeer(config GoShimmerConfig) error {
fmt.Sprintf("--valueLayer.fcob.averageNetworkDelay=%d", ParaFCoBAverageNetworkDelay),
fmt.Sprintf("--autopeering.outboundUpdateIntervalMs=%d", ParaOutboundUpdateIntervalMs),
fmt.Sprintf("--node.disablePlugins=%s", config.DisabledPlugins),
fmt.Sprintf("--pow.difficulty=%d", ParaPoWDifficulty),
fmt.Sprintf("--node.enablePlugins=%s", func() string {
var plugins []string
if config.Bootstrap {
......@@ -95,6 +96,14 @@ func (d *DockerContainer) CreateGoShimmerPeer(config GoShimmerConfig) error {
}
return strings.Join(plugins[:], ",")
}()),
// define the faucet seed in case the faucet dApp is enabled
func() string {
if !config.Faucet {
return ""
}
return fmt.Sprintf("--faucet.seed=%s", genesisSeedBase58)
}(),
fmt.Sprintf("--faucet.tokensPerRequest=%d", ParaFaucetTokensPerRequest),
fmt.Sprintf("--valueLayer.snapshot.file=%s", config.SnapshotFilePath),
fmt.Sprintf("--bootstrap.initialIssuance.timePeriodSec=%d", config.BootstrapInitialIssuanceTimePeriodSec),
"--webapi.bindAddress=0.0.0.0:8080",
......
......@@ -31,11 +31,16 @@ var (
ParaOutboundUpdateIntervalMs = 100
// ParaBootstrapOnEveryNode whether to enable the bootstrap plugin on every node.
ParaBootstrapOnEveryNode = false
// ParaFaucetTokensPerRequest defines the tokens to send up on each faucet request message.
ParaFaucetTokensPerRequest int64 = 1337
// ParaPoWDifficulty defines the PoW difficulty.
ParaPoWDifficulty = 1
)
var (
genesisSeed = []byte{95, 76, 224, 164, 168, 80, 141, 174, 133, 77, 153, 100, 4, 202, 113,
104, 71, 130, 88, 200, 46, 56, 243, 121, 216, 236, 70, 146, 234, 158, 206, 230}
genesisSeedBase58 = "7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih"
)
//GoShimmerConfig defines the config of a GoShimmer node.
......
package faucet
import (
"testing"
"time"
"github.com/iotaledger/goshimmer/dapps/valuetransfers"
"github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework"
"github.com/iotaledger/goshimmer/tools/integration-tests/tester/tests"
"github.com/stretchr/testify/require"
)
// TestFaucetPersistence sends funds by faucet request.
func TestFaucetPersistence(t *testing.T) {
prevPoWDiff := framework.ParaPoWDifficulty
framework.ParaPoWDifficulty = 0
defer func() {
framework.ParaPoWDifficulty = prevPoWDiff
}()
n, err := f.CreateNetwork("faucet_TestPersistence", 5, 2)
require.NoError(t, err)
defer tests.ShutdownNetwork(t, n)
peers := n.Peers()
// wait for peers to change their state to synchronized
time.Sleep(5 * time.Second)
// master node sends funds to all peers in the network
ids, addrBalance := tests.SendFaucetRequestOnRandomPeer(t, peers[1:], 10)
// wait for messages to be gossiped
time.Sleep(2 * valuetransfers.DefaultAverageNetworkDelay)
// check whether all issued messages are available on all nodes
tests.CheckForMessageIds(t, n.Peers(), ids, true)
// wait for transactions to be gossiped
time.Sleep(2 * valuetransfers.DefaultAverageNetworkDelay)
// check ledger state
tests.CheckBalances(t, peers[1:], addrBalance)
// stop all nodes
for _, peer := range n.Peers() {
err = peer.Stop()
require.NoError(t, err)
}
// start all nodes
for _, peer := range n.Peers() {
err = peer.Start()
require.NoError(t, err)
}
// wait for peers to start
time.Sleep(20 * time.Second)
// check whether all issued messages are available on all nodes
tests.CheckForMessageIds(t, n.Peers(), ids, true)
// check ledger state
tests.CheckBalances(t, peers[1:], addrBalance)
}
package faucet
import (
"os"
"testing"
"github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework"
)
var f *framework.Framework
// TestMain gets called by the test utility and is executed before any other test in this package.
// It is therefore used to initialize the integration testing framework.
func TestMain(m *testing.M) {
var err error
f, err = framework.Instance()
if err != nil {
panic(err)
}
// call the tests
os.Exit(m.Run())
}
......@@ -9,6 +9,7 @@ import (
"testing"
"time"
faucet_payload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
......@@ -76,6 +77,42 @@ func SendDataMessage(t *testing.T, peer *framework.Peer, data []byte, number int
return id, sent
}
// SendFaucetRequestOnRandomPeer sends a faucet request on a given peer and returns the id and a DataMessageSent struct.
func SendFaucetRequestOnRandomPeer(t *testing.T, peers []*framework.Peer, numMessages int) (ids map[string]DataMessageSent, addrBalance map[string]map[balance.Color]int64) {
ids = make(map[string]DataMessageSent, numMessages)
addrBalance = make(map[string]map[balance.Color]int64)
for _, p := range peers {
addr := p.Seed().Address(0).String()
addrBalance[addr] = make(map[balance.Color]int64)
addrBalance[addr][balance.ColorIOTA] = 0
}
for i := 0; i < numMessages; i++ {
peer := peers[rand.Intn(len(peers))]
id, sent := SendFaucetRequest(t, peer)
ids[id] = sent
addrBalance[peer.Seed().Address(0).String()][balance.ColorIOTA] += framework.ParaFaucetTokensPerRequest
}
return ids, addrBalance
}
// SendFaucetRequest sends a data message on a given peer and returns the id and a DataMessageSent struct.
func SendFaucetRequest(t *testing.T, peer *framework.Peer) (string, DataMessageSent) {
addr := peer.Seed().Address(0)
resp, err := peer.SendFaucetRequest(addr.String())
require.NoErrorf(t, err, "Could not send faucet request on %s", peer.String())
sent := DataMessageSent{
id: resp.ID,
// save payload to be able to compare API response
data: faucet_payload.New(addr).Bytes(),
issuerPublicKey: peer.Identity.PublicKey().String(),
}
return resp.ID, sent
}
// CheckForMessageIds performs checks to make sure that all peers received all given messages defined in ids.
func CheckForMessageIds(t *testing.T, peers []*framework.Peer, ids map[string]DataMessageSent, checkSynchronized bool) {
var idsSlice []string
......
package main
import (
"fmt"
"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet"
)
func main() {
fmt.Println(wallet.New().Seed().Address(0))
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment