-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Hendrik Amler <hendrik@perun.network>
- Loading branch information
Showing
15 changed files
with
1,295 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Based on Preview 7 | ||
# https://soroban.stellar.org/docs/reference/releases | ||
|
||
FROM ubuntu:20.04 | ||
|
||
RUN apt update && apt install -y curl | ||
|
||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rust_install.sh | ||
RUN sh rust_install.sh -y | ||
RUN echo $PATH | ||
ENV PATH="$PATH:/root/.cargo/bin" | ||
RUN rustup target add wasm32-unknown-unknown | ||
|
||
RUN apt install -y build-essential | ||
# WORKDIR / | ||
RUN mkdir /workspace | ||
WORKDIR /workspace | ||
ENV IS_USING_DOCKER=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<h2 align="center">Perun Stellar Example</h2> | ||
|
||
This example shows how to set up a payment channel on Stellar, which utilizes the [go-perun](https://github.com/perun-network/go-perun) channel library, and also the [Stellar payment channel backend](https://github.com/perun-network/perun-stellar-backend). | ||
|
||
# Setup | ||
|
||
Spin up the local Stellar blockchain, serving as a local testnet for demonstration purposes. | ||
|
||
``` | ||
$ ./quickstart.sh standalone | ||
``` | ||
|
||
This will start the Stellar, Horizon and Soroban nodes in the background. This is the platform on which we deploy the Stellar Asset Contract (SAC), and the Perun Payment Channel contract. This allows us to create and utilize L2 channels on Stellar for any customized Stellar asset tokens. | ||
|
||
# Using the example | ||
|
||
You can start the demo by simply running | ||
|
||
``` | ||
$ go run main.go | ||
``` | ||
|
||
The accounts for Alice and Bob used in the example are generated randomly and funded at the initialization stage of the demo. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"math/big" | ||
|
||
"perun.network/go-perun/channel" | ||
"perun.network/go-perun/client" | ||
) | ||
|
||
type PaymentChannel struct { | ||
ch *client.Channel | ||
currencies []channel.Asset | ||
} | ||
|
||
func (c *PaymentChannel) GetChannel() *client.Channel { | ||
return c.ch | ||
} | ||
func (c *PaymentChannel) GetChannelParams() *channel.Params { | ||
return c.ch.Params() | ||
} | ||
|
||
func (c *PaymentChannel) GetChannelState() *channel.State { | ||
return c.ch.State() | ||
} | ||
|
||
func newPaymentChannel(ch *client.Channel, currencies []channel.Asset) *PaymentChannel { | ||
return &PaymentChannel{ | ||
ch: ch, | ||
currencies: currencies, | ||
} | ||
} | ||
|
||
// SendPayment sends a payment to the channel peer. | ||
func (c PaymentChannel) SendPayment(amount int64, assetIdx int) { | ||
// Transfer the given amount from us to peer. | ||
// Use UpdateBy to update the channel state. | ||
err := c.ch.Update(context.TODO(), func(state *channel.State) { | ||
icp := big.NewInt(amount) | ||
actor := c.ch.Idx() | ||
peer := 1 - actor | ||
state.Allocation.TransferBalance(actor, peer, c.currencies[assetIdx], icp) | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
// PerformSwap performs a swap by "swapping" the balances of the two | ||
// participants for both assets. | ||
func (c PaymentChannel) PerformSwap() { | ||
err := c.ch.Update(context.TODO(), func(state *channel.State) { // We use context.TODO to keep the code simple. | ||
// We simply swap the balances for the two assets. | ||
state.Balances = channel.Balances{ | ||
{state.Balances[0][1], state.Balances[0][0]}, | ||
{state.Balances[1][1], state.Balances[1][0]}, | ||
} | ||
|
||
// Set the state to final because we do not expect any other updates | ||
// than this swap. | ||
state.IsFinal = true | ||
}) | ||
if err != nil { | ||
panic(err) // We panic on error to keep the code simple. | ||
} | ||
} | ||
|
||
// Settle settles the payment channel and withdraws the funds. | ||
func (c PaymentChannel) Settle() { | ||
// If the channel is not finalized: Finalize the channel to enable fast settlement. | ||
|
||
if !c.ch.State().IsFinal { | ||
err := c.ch.Update(context.TODO(), func(state *channel.State) { | ||
state.IsFinal = true | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
// Settle concludes the channel and withdraws the funds. | ||
err := c.ch.Settle(context.TODO(), false) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Close frees up channel resources. | ||
c.ch.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package client | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"math/big" | ||
|
||
"perun.network/go-perun/wire/net/simple" | ||
|
||
"context" | ||
"errors" | ||
|
||
pchannel "perun.network/go-perun/channel" | ||
pclient "perun.network/go-perun/client" | ||
"perun.network/go-perun/watcher/local" | ||
"perun.network/go-perun/wire" | ||
"perun.network/perun-stellar-backend/channel" | ||
"perun.network/perun-stellar-backend/wallet" | ||
) | ||
|
||
type PaymentClient struct { | ||
perunClient *pclient.Client | ||
account *wallet.Account | ||
currencies []pchannel.Asset | ||
channels chan *PaymentChannel | ||
Channel *PaymentChannel | ||
wAddr wire.Address | ||
balance *big.Int | ||
} | ||
|
||
func SetupPaymentClient( | ||
w *wallet.EphemeralWallet, | ||
acc *wallet.Account, | ||
stellarTokenIDs []pchannel.Asset, | ||
bus *wire.LocalBus, | ||
funder *channel.Funder, | ||
adj *channel.Adjudicator, | ||
|
||
) (*PaymentClient, error) { | ||
watcher, err := local.NewWatcher(adj) | ||
if err != nil { | ||
return nil, fmt.Errorf("intializing watcher: %w", err) | ||
} | ||
// Setup Perun client. | ||
wireAddr := simple.NewAddress(acc.Address().String()) | ||
perunClient, err := pclient.New(wireAddr, bus, funder, adj, w, watcher) | ||
if err != nil { | ||
return nil, errors.New("creating client") | ||
} | ||
|
||
c := &PaymentClient{ | ||
perunClient: perunClient, | ||
account: acc, | ||
currencies: stellarTokenIDs, | ||
channels: make(chan *PaymentChannel, 1), | ||
wAddr: wireAddr, | ||
balance: big.NewInt(0), | ||
} | ||
|
||
go perunClient.Handle(c, c) | ||
return c, nil | ||
} | ||
|
||
// startWatching starts the dispute watcher for the specified channel. | ||
func (c *PaymentClient) startWatching(ch *pclient.Channel) { | ||
go func() { | ||
err := ch.Watch(c) | ||
if err != nil { | ||
log.Printf("watcher returned with error: %v", err) | ||
} | ||
}() | ||
} | ||
|
||
func (c *PaymentClient) OpenChannel(peer wire.Address, balances pchannel.Balances) { | ||
// We define the channel participants. The proposer has always index 0. Here | ||
// we use the on-chain addresses as off-chain addresses, but we could also | ||
// use different ones. | ||
|
||
participants := []wire.Address{c.WireAddress(), peer} | ||
|
||
initAlloc := pchannel.NewAllocation(2, c.currencies...) | ||
initAlloc.Balances = balances | ||
|
||
// Prepare the channel proposal by defining the channel parameters. | ||
challengeDuration := uint64(10) // On-chain challenge duration in seconds. | ||
proposal, err := pclient.NewLedgerChannelProposal( | ||
challengeDuration, | ||
c.account.Address(), | ||
initAlloc, | ||
participants, | ||
) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Send the proposal. | ||
ch, err := c.perunClient.ProposeChannel(context.TODO(), proposal) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Start the on-chain event watcher. It automatically handles disputes. | ||
c.startWatching(ch) | ||
c.Channel = newPaymentChannel(ch, c.currencies) | ||
} | ||
|
||
func (p *PaymentClient) WireAddress() wire.Address { | ||
return p.wAddr | ||
} | ||
|
||
func (c *PaymentClient) AcceptedChannel() *PaymentChannel { | ||
return <-c.channels | ||
} | ||
|
||
// Shutdown gracefully shuts down the client. | ||
func (c *PaymentClient) Shutdown() { | ||
c.perunClient.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"math/big" | ||
"time" | ||
|
||
"perun.network/go-perun/channel" | ||
"perun.network/go-perun/client" | ||
) | ||
|
||
// HandleProposal is the callback for incoming channel proposals. | ||
func (c *PaymentClient) HandleProposal(p client.ChannelProposal, r *client.ProposalResponder) { | ||
lcp, err := func() (*client.LedgerChannelProposalMsg, error) { | ||
// Ensure that we got a ledger channel proposal. | ||
lcp, ok := p.(*client.LedgerChannelProposalMsg) | ||
if !ok { | ||
return nil, errors.New("invalid proposal type: expected *client.LedgerChannelProposalMsg") | ||
} | ||
|
||
// Check that we have the correct number of participants. | ||
if lcp.NumPeers() != 2 { | ||
return nil, fmt.Errorf("invalid number of participants: %d", lcp.NumPeers()) | ||
} | ||
|
||
// Check that the channel has the expected assets and funding balances. | ||
const assetIdx, clientIdx, peerIdx = 0, 0, 1 | ||
|
||
pAssets := make([]channel.Asset, len(c.currencies)) | ||
for i, asset := range c.currencies { | ||
pAssets[i] = channel.Asset(asset) | ||
} | ||
|
||
if err := channel.AssertAssetsEqual(lcp.InitBals.Assets, pAssets); err != nil { | ||
return nil, fmt.Errorf("invalid assets: %v", err) | ||
} else if lcp.FundingAgreement[assetIdx][clientIdx].Cmp(big.NewInt(0)) == 0 && lcp.FundingAgreement[assetIdx][peerIdx].Cmp(big.NewInt(0)) == 0 { | ||
return nil, errors.New("invalid funding balance: both client and peer cannot have zero funding") | ||
} | ||
return lcp, nil | ||
}() | ||
if err != nil { | ||
errReject := r.Reject(context.TODO(), err.Error()) | ||
if errReject != nil { | ||
// Log the error or take other action as needed | ||
log.Printf("error rejecting proposal: %v\n", errReject) | ||
} | ||
} | ||
|
||
// Create a channel accept message and send it. | ||
accept := lcp.Accept( | ||
c.account.Address(), // The account we use in the channel. | ||
client.WithRandomNonce(), // Our share of the channel nonce. | ||
) | ||
ch, err := r.Accept(context.TODO(), accept) | ||
if err != nil { | ||
log.Printf("error accepting channel proposal: %v\n", err) | ||
return | ||
} | ||
|
||
// Start the on-chain event watcher. It automatically handles disputes. | ||
c.startWatching(ch) | ||
|
||
// Store channel. | ||
c.channels <- newPaymentChannel(ch, c.currencies) | ||
|
||
} | ||
|
||
// HandleUpdate is the callback for incoming channel updates. | ||
func (c *PaymentClient) HandleUpdate(cur *channel.State, next client.ChannelUpdate, r *client.UpdateResponder) { | ||
// We accept every update that increases our balance. | ||
err := func() error { | ||
err := channel.AssertAssetsEqual(cur.Assets, next.State.Assets) | ||
if err != nil { | ||
return fmt.Errorf("invalid assets: %v", err) | ||
} | ||
|
||
receiverIdx := 1 - next.ActorIdx // This works because we are in a two-party channel. | ||
// curBal0 := cur.Allocation.Balance(receiverIdx, c.currencies[0]) | ||
// nextBal0 := next.State.Allocation.Balance(receiverIdx, c.currencies[0]) | ||
// if nextBal0.Cmp(curBal0) < 0 { | ||
// return fmt.Errorf("Invalid balance: %v", nextBal0) | ||
// } | ||
for _, currency := range c.currencies { | ||
curBal := cur.Allocation.Balance(receiverIdx, currency) | ||
nextBal := next.State.Allocation.Balance(receiverIdx, currency) | ||
if nextBal.Cmp(curBal) < 0 { | ||
return fmt.Errorf("invalid balance for asset %v: %v", currency, nextBal) | ||
} | ||
} | ||
|
||
return nil | ||
}() | ||
if err != nil { | ||
r.Reject(context.TODO(), err.Error()) //nolint:errcheck // It's OK if rejection fails. | ||
} | ||
|
||
// Send the acceptance message. | ||
err = r.Accept(context.TODO()) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
// HandleAdjudicatorEvent is the callback for smart contract events. | ||
func (c *PaymentClient) HandleAdjudicatorEvent(e channel.AdjudicatorEvent) { | ||
log.Printf("Adjudicator event: type = %T, client = %v", e, c.account.Address()) | ||
} | ||
|
||
func (c *PaymentClient) GetChannel() (*PaymentChannel, error) { | ||
select { | ||
case channel := <-c.channels: | ||
c.channels <- channel // Put the channel back into the channels channel. | ||
return channel, nil | ||
case <-time.After(time.Second): // Set a timeout duration (e.g., 1 second). | ||
return nil, errors.New("no channel available") | ||
} | ||
} |
Oops, something went wrong.