Skip to content

Commit

Permalink
add: stellar payment example
Browse files Browse the repository at this point in the history
Signed-off-by: Hendrik Amler <hendrik@perun.network>
  • Loading branch information
tinnendo committed Sep 18, 2024
1 parent ddae35d commit 9c4c65e
Show file tree
Hide file tree
Showing 15 changed files with 1,295 additions and 1 deletion.
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.20'
go-version: '1.22'

- uses: actions/cache@v3
with:
Expand Down Expand Up @@ -71,3 +71,16 @@ jobs:
sleep 5
go run .
docker stop ganache
- name: Payment Channel XLM
working-directory: payment-channel-xlm
run: |
chmod +x ./testdata/docker/build.sh
./testdata/docker/build.sh
chmod +x ./quickstart.sh
./quickstart.sh standalone &
sleep 30
go run ./
18 changes: 18 additions & 0 deletions payment-channel-xlm/Dockerfile
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
23 changes: 23 additions & 0 deletions payment-channel-xlm/README.md
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.
89 changes: 89 additions & 0 deletions payment-channel-xlm/client/channel.go
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()
}
118 changes: 118 additions & 0 deletions payment-channel-xlm/client/client.go
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()
}
120 changes: 120 additions & 0 deletions payment-channel-xlm/client/handle.go
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")
}
}
Loading

0 comments on commit 9c4c65e

Please sign in to comment.