Add integration tests and regtest support (#128)

* regtest support + integration tests (e2e)

* add integration CI

* add PR trigger on integration CI

* wait for ocean to be unlocked at startup

* integration tests: add tests flags and build docker images at startup

* use nigiri chopsticks-liquid

* fix after reviews

* Update client/init.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* do not trigger integration on PR

---------

Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>
Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
This commit is contained in:
Louis Singer
2024-04-19 18:57:13 +02:00
committed by GitHub
parent f9e7621165
commit 852756eaba
26 changed files with 633 additions and 161 deletions

View File

@@ -20,6 +20,10 @@ jobs:
go-version: ">1.17.2"
- uses: actions/checkout@v3
- run: go get -v -t -d ./...
- name: Run Nigiri
uses: vulpemventures/nigiri-github-action@v1
- name: integration testing
run: make integrationtest

View File

@@ -20,6 +20,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
@@ -34,6 +35,13 @@ const (
DUST = 450
)
var passwordFlag = cli.StringFlag{
Name: "password",
Usage: "password to unlock the wallet",
Required: false,
Hidden: true,
}
func hashPassword(password []byte) []byte {
hash := sha256.Sum256(password)
return hash[:]
@@ -64,22 +72,30 @@ func verifyPassword(password []byte) error {
return nil
}
func readPassword() ([]byte, error) {
fmt.Print("unlock your wallet with password: ")
passwordInput, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // new line
if err != nil {
return nil, err
func readPassword(ctx *cli.Context, verify bool) ([]byte, error) {
password := []byte(ctx.String("password"))
if len(password) == 0 {
fmt.Print("unlock your wallet with password: ")
var err error
password, err = term.ReadPassword(int(syscall.Stdin))
fmt.Println() // new line
if err != nil {
return nil, err
}
}
if err := verifyPassword(passwordInput); err != nil {
return nil, err
if verify {
if err := verifyPassword(password); err != nil {
return nil, err
}
}
return passwordInput, nil
return password, nil
}
func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
func privateKeyFromPassword(ctx *cli.Context) (*secp256k1.PrivateKey, error) {
state, err := getState()
if err != nil {
return nil, err
@@ -95,7 +111,7 @@ func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
return nil, fmt.Errorf("invalid encrypted private key: %s", err)
}
password, err := readPassword()
password, err := readPassword(ctx, true)
if err != nil {
return nil, err
}
@@ -256,9 +272,25 @@ func getOffchainBalance(
return balance, amountByExpiration, nil
}
func getBaseURL() (string, error) {
state, err := getState()
if err != nil {
return "", err
}
baseURL := state[EXPLORER]
if len(baseURL) <= 0 {
return "", fmt.Errorf("missing explorer base url")
}
return baseURL, nil
}
func getTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
_, net := getNetwork()
baseUrl := explorerUrl[net.Name]
baseUrl, err := getBaseURL()
if err != nil {
return false, 0, err
}
resp, err := http.Get(fmt.Sprintf("%s/tx/%s", baseUrl, txid))
if err != nil {
return false, 0, err
@@ -301,9 +333,16 @@ func getNetwork() (*common.Network, *network.Network) {
if !ok {
return &common.MainNet, &network.Liquid
}
return networkFromString(net)
}
func networkFromString(net string) (*common.Network, *network.Network) {
if net == "testnet" {
return &common.TestNet, &network.Testnet
}
if net == "regtest" {
return &common.RegTest, &network.Regtest
}
return &common.MainNet, &network.Liquid
}
@@ -432,11 +471,13 @@ func handleRoundStream(
return "", err
}
// validate the congestion tree
if err := tree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime),
); err != nil {
return "", err
if !isOnchainOnly(receivers) {
// validate the congestion tree
if err := tree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime),
); err != nil {
return "", err
}
}
if err := common.ValidateConnectors(poolTx, connectors); err != nil {
@@ -772,7 +813,7 @@ func getRedeemBranches(
}
redeemBranch, err := newRedeemBranch(
ctx, explorer, congestionTrees[vtxo.poolTxid], vtxo,
explorer, congestionTrees[vtxo.poolTxid], vtxo,
)
if err != nil {
return nil, err
@@ -1060,15 +1101,10 @@ func addInputs(
return err
}
witnessUtxo := transaction.TxOutput{
Asset: assetID,
Value: value,
Script: script,
Nonce: []byte{0x00},
}
witnessUtxo := transaction.NewTxOutput(assetID, value, script)
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
len(updater.Pset.Inputs)-1, witnessUtxo,
); err != nil {
return err
}
@@ -1077,3 +1113,18 @@ func addInputs(
return nil
}
func isOnchainOnly(receivers []*arkv1.Output) bool {
for _, receiver := range receivers {
isOnChain, _, _, err := decodeReceiverAddress(receiver.Address)
if err != nil {
continue
}
if !isOnChain {
return false
}
}
return true
}

View File

@@ -10,10 +10,11 @@ var dumpCommand = cli.Command{
Name: "dump-privkey",
Usage: "Dumps private key of the Ark wallet",
Action: dumpAction,
Flags: []cli.Flag{&passwordFlag},
}
func dumpAction(ctx *cli.Context) error {
privateKey, err := privateKeyFromPassword()
privateKey, err := privateKeyFromPassword(ctx)
if err != nil {
return err
}

View File

@@ -40,8 +40,10 @@ type explorer struct {
}
func NewExplorer() Explorer {
_, net := getNetwork()
baseUrl := explorerUrl[net.Name]
baseUrl, err := getBaseURL()
if err != nil {
panic(err)
}
return &explorer{
cache: make(map[string]string),
@@ -76,10 +78,7 @@ func (e *explorer) Broadcast(txStr string) (string, error) {
if err != nil {
return "", err
}
txStr, err = tx.ToHex()
if err != nil {
return "", err
}
txStr, _ = tx.ToHex()
}
txid := tx.TxHash().String()
e.cache[txid] = txStr

View File

@@ -4,6 +4,8 @@ import (
"context"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"strings"
@@ -11,15 +13,10 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/network"
)
var (
passwordFlag = cli.StringFlag{
Name: "password",
Usage: "password to encrypt private key",
Required: true,
}
privateKeyFlag = cli.StringFlag{
Name: "prvkey",
Usage: "optional, private key to encrypt",
@@ -34,35 +31,54 @@ var (
Usage: "the url of the ASP to connect to",
Required: true,
}
explorerFlag = cli.StringFlag{
Name: "explorer",
Usage: "the url of the explorer to use",
}
)
var initCommand = cli.Command{
Name: "init",
Usage: "Initialize your Ark wallet with an encryption password, and connect it to an ASP",
Action: initAction,
Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag},
Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag, &explorerFlag},
}
func initAction(ctx *cli.Context) error {
key := ctx.String("prvkey")
password := ctx.String("password")
net := strings.ToLower(ctx.String("network"))
url := ctx.String("ark-url")
explorer := ctx.String("explorer")
var explorerURL string
if len(password) <= 0 {
return fmt.Errorf("invalid password")
}
if len(url) <= 0 {
return fmt.Errorf("invalid ark url")
}
if net != "mainnet" && net != "testnet" {
if net != "mainnet" && net != "testnet" && net != "regtest" {
return fmt.Errorf("invalid network")
}
if err := connectToAsp(ctx.Context, net, url); err != nil {
if len(explorer) > 0 {
explorerURL = explorer
_, network := networkFromString(net)
if err := testEsploraEndpoint(network, explorerURL); err != nil {
return fmt.Errorf("failed to connect with explorer: %s", err)
}
} else {
explorerURL = explorerUrl[net]
}
if err := connectToAsp(ctx.Context, net, url, explorerURL); err != nil {
return err
}
return initWallet(ctx, key, password)
password, err := readPassword(ctx, false)
if err != nil {
return err
}
return initWallet(key, password)
}
func generateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
@@ -73,7 +89,7 @@ func generateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
return privKey, nil
}
func connectToAsp(ctx context.Context, net, url string) error {
func connectToAsp(ctx context.Context, net, url, explorer string) error {
client, close, err := getClient(url)
if err != nil {
return err
@@ -91,10 +107,11 @@ func connectToAsp(ctx context.Context, net, url string) error {
ASP_PUBKEY: resp.Pubkey,
ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
EXPLORER: explorer,
})
}
func initWallet(ctx *cli.Context, key, password string) error {
func initWallet(key string, password []byte) error {
var privateKey *secp256k1.PrivateKey
if len(key) <= 0 {
privKey, err := generateRandomPrivateKey()
@@ -113,7 +130,7 @@ func initWallet(ctx *cli.Context, key, password string) error {
cypher := newAES128Cypher()
buf := privateKey.Serialize()
encryptedPrivateKey, err := cypher.encrypt(buf, []byte(password))
encryptedPrivateKey, err := cypher.encrypt(buf, password)
if err != nil {
return err
}
@@ -134,3 +151,20 @@ func initWallet(ctx *cli.Context, key, password string) error {
fmt.Println("wallet initialized")
return nil
}
func testEsploraEndpoint(net *network.Network, url string) error {
resp, err := http.Get(fmt.Sprintf("%s/asset/%s", url, net.AssetID))
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(string(body))
}
return nil
}

View File

@@ -26,6 +26,7 @@ const (
PASSWORD_HASH = "password_hash"
PUBKEY = "public_key"
NETWORK = "network"
EXPLORER = "explorer"
)
var (
@@ -36,6 +37,7 @@ var (
explorerUrl = map[string]string{
network.Liquid.Name: "https://blockstream.info/liquid/api",
network.Testnet.Name: "https://blockstream.info/liquidtestnet/api",
network.Regtest.Name: "http://localhost:3001",
}
initialState = map[string]string{

View File

@@ -27,7 +27,7 @@ var onboardCommand = cli.Command{
Name: "onboard",
Usage: "Onboard the Ark by lifting your funds",
Action: onboardAction,
Flags: []cli.Flag{&amountOnboardFlag},
Flags: []cli.Flag{&amountOnboardFlag, &passwordFlag},
}
func onboardAction(ctx *cli.Context) error {
@@ -92,11 +92,9 @@ func onboardAction(ctx *cli.Context) error {
return err
}
explorer := NewExplorer()
txid, err := explorer.Broadcast(pset)
if err != nil {
return err
}
ptx, _ := psetv2.NewPsetFromBase64(pset)
utx, _ := ptx.UnsignedTx()
txid := utx.TxHash().String()
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
Txid: txid,

View File

@@ -2,7 +2,6 @@ package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
@@ -40,7 +39,7 @@ var (
var redeemCommand = cli.Command{
Name: "redeem",
Usage: "Redeem your offchain funds, either collaboratively or unilaterally",
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag, &enableExpiryCoinselectFlag},
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag, &passwordFlag, &enableExpiryCoinselectFlag},
Action: redeemAction,
}
@@ -68,7 +67,7 @@ func redeemAction(ctx *cli.Context) error {
fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n")
}
return unilateralRedeem(ctx.Context, client)
return unilateralRedeem(ctx, client)
}
return collaborativeRedeem(ctx, client, addr, amount)
@@ -137,7 +136,7 @@ func collaborativeRedeem(
})
}
secKey, err := privateKeyFromPassword()
secKey, err := privateKeyFromPassword(ctx)
if err != nil {
return err
}
@@ -178,14 +177,14 @@ func collaborativeRedeem(
return nil
}
func unilateralRedeem(ctx context.Context, client arkv1.ArkServiceClient) error {
func unilateralRedeem(ctx *cli.Context, client arkv1.ArkServiceClient) error {
offchainAddr, _, _, err := getAddress()
if err != nil {
return err
}
explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, false)
vtxos, err := getVtxos(ctx.Context, explorer, client, offchainAddr, false)
if err != nil {
return err
}
@@ -196,16 +195,18 @@ func unilateralRedeem(ctx context.Context, client arkv1.ArkServiceClient) error
totalVtxosAmount += vtxo.amount
}
ok := askForConfirmation(fmt.Sprintf("redeem %d sats ?", totalVtxosAmount))
if !ok {
return fmt.Errorf("aborting unilateral exit")
if len(ctx.String("password")) == 0 {
ok := askForConfirmation(fmt.Sprintf("redeem %d sats ?", totalVtxosAmount))
if !ok {
return fmt.Errorf("aborting unilateral exit")
}
}
// transactionsMap avoid duplicates
transactionsMap := make(map[string]struct{}, 0)
transactions := make([]string, 0)
redeemBranches, err := getRedeemBranches(ctx, explorer, client, vtxos)
redeemBranches, err := getRedeemBranches(ctx.Context, explorer, client, vtxos)
if err != nil {
return err
}

View File

@@ -47,7 +47,7 @@ var sendCommand = cli.Command{
Name: "send",
Usage: "Send your onchain or offchain funds to one or many receivers",
Action: sendAction,
Flags: []cli.Flag{&receiversFlag, &toFlag, &amountFlag, &enableExpiryCoinselectFlag},
Flags: []cli.Flag{&receiversFlag, &toFlag, &amountFlag, &passwordFlag, &enableExpiryCoinselectFlag},
}
func sendAction(ctx *cli.Context) error {
@@ -186,7 +186,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
})
}
secKey, err := privateKeyFromPassword()
secKey, err := privateKeyFromPassword(ctx)
if err != nil {
return err
}
@@ -219,7 +219,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
})
}
func sendOnchain(_ *cli.Context, receivers []receiver) (string, error) {
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return "", err
@@ -349,7 +349,7 @@ func sendOnchain(_ *cli.Context, receivers []receiver) (string, error) {
return "", err
}
prvKey, err := privateKeyFromPassword()
prvKey, err := privateKeyFromPassword(ctx)
if err != nil {
return "", err
}

View File

@@ -1,7 +1,6 @@
package main
import (
"context"
"fmt"
"time"
@@ -22,7 +21,7 @@ type redeemBranch struct {
}
func newRedeemBranch(
ctx context.Context, explorer Explorer,
explorer Explorer,
congestionTree tree.CongestionTree, vtxo vtxo,
) (*redeemBranch, error) {
sweepClosure, seconds, err := findSweepClosure(congestionTree)

View File

@@ -14,3 +14,8 @@ var TestNet = Network{
Name: "testnet",
Addr: "tark",
}
var RegTest = Network{
Name: "regtest",
Addr: TestNet.Addr,
}

View File

@@ -0,0 +1,47 @@
version: "3.7"
services:
oceand:
container_name: oceand
image: ghcr.io/vulpemventures/oceand:latest
restart: unless-stopped
user: 0:0
environment:
- OCEAN_LOG_LEVEL=5
- OCEAN_NO_TLS=true
- OCEAN_NO_PROFILER=true
- OCEAN_ELECTRUM_URL=tcp://electrs-liquid:50001
- OCEAN_NETWORK=regtest
- OCEAN_UTXO_EXPIRY_DURATION_IN_SECONDS=60
- OCEAN_DB_TYPE=badger
ports:
- "18000:18000"
arkd:
container_name: arkd
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
- oceand
environment:
- ARK_WALLET_ADDR=oceand:18000
- ARK_ROUND_INTERVAL=10
- ARK_NETWORK=regtest
ports:
- "6000:6000"
volumes:
oceand:
external: false
ocean:
external: false
arkd:
external: false
ark:
external: false
networks:
default:
name: nigiri
external: true

View File

@@ -23,7 +23,7 @@ help:
## intergrationtest: runs integration tests
integrationtest:
@echo "Running integration tests..."
@find . -name go.mod -execdir go test -v -count=1 -race $(go list ./... | grep internal/test) \;
@go test -v -count=1 -race -timeout 200s github.com/ark-network/ark/test/e2e
## lint: lint codebase
lint:
@@ -40,7 +40,7 @@ run: clean
## test: runs unit and component tests
test:
@echo "Running unit tests..."
@find . -name go.mod -execdir go test -v -count=1 -race ./... $(go list ./... | grep -v internal/test) \;
@find . -name go.mod -execdir go test -v -count=1 -race ./internal/... \;
## vet: code analysis
vet:

View File

@@ -69,8 +69,8 @@ func (c *Config) Validate() error {
if c.RoundInterval < 2 {
return fmt.Errorf("invalid round interval, must be at least 2 seconds")
}
if c.Network.Name != "liquid" && c.Network.Name != "testnet" {
return fmt.Errorf("invalid network, must be either liquid or testnet")
if c.Network.Name != "liquid" && c.Network.Name != "testnet" && c.Network.Name != "regtest" {
return fmt.Errorf("invalid network, must be liquid, testnet or regtest")
}
if len(c.WalletAddr) <= 0 {
return fmt.Errorf("missing onchain wallet address")
@@ -239,11 +239,14 @@ func (c *Config) appService() error {
}
func (c *Config) mainChain() network.Network {
net := network.Liquid
if c.Network.Name != "mainnet" {
net = network.Testnet
switch c.Network.Name {
case "testnet":
return network.Testnet
case "regtest":
return network.Regtest
default:
return network.Liquid
}
return net
}
type supportedType map[string]struct{}

View File

@@ -121,7 +121,9 @@ func getNetwork() (common.Network, error) {
return common.MainNet, nil
case "testnet":
return common.TestNet, nil
case "regtest":
return common.RegTest, nil
default:
return common.Network{}, fmt.Errorf("unknown network")
return common.Network{}, fmt.Errorf("unknown network %s", viper.GetString(Network))
}
}

View File

@@ -415,7 +415,7 @@ func (s *service) handleOnboarding(onboarding onboarding) {
}
if err != nil || !isConfirmed {
time.Sleep(30 * time.Second)
time.Sleep(5 * time.Second)
}
}
}
@@ -614,28 +614,30 @@ func (s *service) updateVtxoSet(round *domain.Round) {
}
newVtxos := s.getNewVtxos(round)
for {
if err := repo.AddVtxos(ctx, newVtxos); err != nil {
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
time.Sleep(100 * time.Millisecond)
continue
}
log.Debugf("added %d new vtxos", len(newVtxos))
break
}
go func() {
if len(newVtxos) > 0 {
for {
if err := s.startWatchingVtxos(newVtxos); err != nil {
log.WithError(err).Warn(
"failed to start watching vtxos, retrying in a moment...",
)
if err := repo.AddVtxos(ctx, newVtxos); err != nil {
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
time.Sleep(100 * time.Millisecond)
continue
}
log.Debugf("started watching %d vtxos", len(newVtxos))
return
log.Debugf("added %d new vtxos", len(newVtxos))
break
}
}()
go func() {
for {
if err := s.startWatchingVtxos(newVtxos); err != nil {
log.WithError(err).Warn(
"failed to start watching vtxos, retrying in a moment...",
)
continue
}
log.Debugf("started watching %d vtxos", len(newVtxos))
return
}
}()
}
}
func (s *service) propagateEvents(round *domain.Round) {
@@ -673,6 +675,10 @@ func (s *service) scheduleSweepVtxosForRound(round *domain.Round) {
}
func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo {
if len(round.CongestionTree) <= 0 {
return nil
}
leaves := round.CongestionTree.Leaves()
vtxos := make([]domain.Vtxo, 0)
for _, node := range leaves {

View File

@@ -71,6 +71,11 @@ func (s *sweeper) removeTask(treeRootTxid string) {
func (s *sweeper) schedule(
expirationTimestamp int64, roundTxid string, congestionTree tree.CongestionTree,
) error {
if len(congestionTree) <= 0 { // skip
log.Debugf("skipping sweep scheduling (round tx %s), empty congestion tree", roundTxid)
return nil
}
root, err := congestionTree.Root()
if err != nil {
return err

View File

@@ -181,12 +181,6 @@ func (r *Round) RegisterPayments(payments []Payment) ([]RoundEvent, error) {
}
func (r *Round) StartFinalization(connectorAddress string, connectors []string, congestionTree tree.CongestionTree, poolTx string) ([]RoundEvent, error) {
if len(connectors) <= 0 {
return nil, fmt.Errorf("missing list of connectors")
}
if len(congestionTree) <= 0 {
return nil, fmt.Errorf("missing congestion tree")
}
if len(poolTx) <= 0 {
return nil, fmt.Errorf("missing unsigned pool tx")
}

View File

@@ -323,32 +323,6 @@ func testStartFinalization(t *testing.T) {
poolTx string
expectedErr string
}{
{
round: &domain.Round{
Id: "0",
Stage: domain.Stage{
Code: domain.RegistrationStage,
},
Payments: paymentsById,
},
connectors: nil,
tree: congestionTree,
poolTx: poolTx,
expectedErr: "missing list of connectors",
},
{
round: &domain.Round{
Id: "0",
Stage: domain.Stage{
Code: domain.RegistrationStage,
},
Payments: paymentsById,
},
connectors: connectors,
tree: nil,
poolTx: poolTx,
expectedErr: "missing congestion tree",
},
{
round: &domain.Round{
Id: "0",

View File

@@ -95,7 +95,7 @@ func deserializeEvent(buf []byte) (domain.RoundEvent, error) {
}
{
var event = domain.RoundFinalizationStarted{}
if err := json.Unmarshal(buf, &event); err == nil && len(event.CongestionTree) > 0 {
if err := json.Unmarshal(buf, &event); err == nil && len(event.Connectors) > 0 {
return event, nil
}
}

View File

@@ -2,9 +2,9 @@ package oceanwallet
import (
"context"
"fmt"
"io"
"strings"
"time"
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
"github.com/ark-network/ark/internal/core/domain"
@@ -47,12 +47,21 @@ func NewService(addr string) (ports.WalletService, error) {
}
ctx := context.Background()
status, err := svc.Status(ctx)
if err != nil {
return nil, err
}
if !(status.IsInitialized() && status.IsUnlocked()) {
return nil, fmt.Errorf("wallet must be already initialized and unlocked")
isReady := false
for !isReady {
status, err := svc.Status(ctx)
if err != nil {
return nil, err
}
isReady = status.IsInitialized() && status.IsUnlocked()
if !isReady {
log.Info("Wallet must be initialized and unlocked to proceed. Waiting for wallet to be ready...")
time.Sleep(3 * time.Second)
}
}
// Create ark account at startup if needed.

View File

@@ -119,11 +119,18 @@ func (b *txBuilder) BuildPoolTx(
// generated in the process and takes the shared utxo outpoint as argument.
// This is safe as the memory allocated for `craftCongestionTree` is freed
// only after `BuildPoolTx` returns.
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
b.net.AssetID, aspPubkey, getOffchainReceivers(payments), minRelayFee, b.roundLifetime, b.exitDelay,
)
if err != nil {
return
var sharedOutputScript []byte
var sharedOutputAmount uint64
var treeFactoryFn tree.TreeFactory
if !isOnchainOnly(payments) {
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree(
b.net.AssetID, aspPubkey, getOffchainReceivers(payments), minRelayFee, b.roundLifetime, b.exitDelay,
)
if err != nil {
return
}
}
connectorAddress, err = b.wallet.DeriveConnectorAddress(context.Background())
@@ -143,12 +150,14 @@ func (b *txBuilder) BuildPoolTx(
return
}
tree, err := treeFactoryFn(psetv2.InputArgs{
Txid: unsignedTx.TxHash().String(),
TxIndex: 0,
})
if err != nil {
return
if treeFactoryFn != nil {
congestionTree, err = treeFactoryFn(psetv2.InputArgs{
Txid: unsignedTx.TxHash().String(),
TxIndex: 0,
})
if err != nil {
return
}
}
poolTx, err = ptx.ToBase64()
@@ -156,7 +165,6 @@ func (b *txBuilder) BuildPoolTx(
return
}
congestionTree = tree
return
}
@@ -219,21 +227,26 @@ func (b *txBuilder) createPoolTx(
if nbOfInputs > 1 {
connectorsAmount -= minRelayFee
}
targetAmount := sharedOutputAmount + connectorsAmount
targetAmount := connectorsAmount
outputs := []psetv2.OutputArgs{
{
outputs := make([]psetv2.OutputArgs, 0)
if sharedOutputScript != nil && sharedOutputAmount > 0 {
targetAmount += sharedOutputAmount
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.net.AssetID,
Amount: sharedOutputAmount,
Script: sharedOutputScript,
},
{
Asset: b.net.AssetID,
Amount: connectorsAmount,
Script: connectorScript,
},
})
}
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.net.AssetID,
Amount: connectorsAmount,
Script: connectorScript,
})
for _, receiver := range receivers {
targetAmount += receiver.Amount

View File

@@ -139,3 +139,14 @@ func addInputs(
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}
func isOnchainOnly(payments []domain.Payment) bool {
for _, p := range payments {
for _, r := range p.Receivers {
if !r.IsOnchain() {
return false
}
}
}
return true
}

205
server/test/e2e/e2e_test.go Normal file
View File

@@ -0,0 +1,205 @@
package e2e
import (
"encoding/json"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
const composePath = "../../../docker-compose.regtest.yml"
func TestMain(m *testing.M) {
_, err := runCommand("docker-compose", "-f", composePath, "up", "-d", "--build")
if err != nil {
fmt.Printf("error starting docker-compose: %s", err)
os.Exit(1)
}
_, err = runOceanCommand("config", "init", "--no-tls")
if err != nil {
fmt.Printf("error initializing ocean config: %s", err)
os.Exit(1)
}
_, err = runOceanCommand("wallet", "create", "--password", password)
if err != nil {
fmt.Printf("error creating ocean wallet: %s", err)
os.Exit(1)
}
_, err = runOceanCommand("wallet", "unlock", "--password", password)
if err != nil {
fmt.Printf("error unlocking ocean wallet: %s", err)
os.Exit(1)
}
_, err = runOceanCommand("account", "create", "--label", "ark", "--unconf")
if err != nil {
fmt.Printf("error creating ocean account: %s", err)
os.Exit(1)
}
addrJSON, err := runOceanCommand("account", "derive", "--account-name", "ark")
if err != nil {
fmt.Printf("error deriving ocean account: %s", err)
os.Exit(1)
}
var addr struct {
Addresses []string `json:"addresses"`
}
if err := json.Unmarshal([]byte(addrJSON), &addr); err != nil {
fmt.Printf("error unmarshalling ocean account: %s (%s)", err, addrJSON)
os.Exit(1)
}
_, err = runCommand("nigiri", "faucet", "--liquid", addr.Addresses[0])
if err != nil {
fmt.Printf("error funding ocean account: %s", err)
os.Exit(1)
}
time.Sleep(2 * time.Second)
_, err = runArkCommand("init", "--ark-url", "localhost:6000", "--password", password, "--network", "regtest", "--explorer", "http://chopsticks-liquid:3000")
if err != nil {
fmt.Printf("error initializing ark config: %s", err)
os.Exit(1)
}
var receive arkReceive
receiveStr, err := runArkCommand("receive")
if err != nil {
fmt.Printf("error getting ark receive addresses: %s", err)
os.Exit(1)
}
if err := json.Unmarshal([]byte(receiveStr), &receive); err != nil {
fmt.Printf("error unmarshalling ark receive addresses: %s", err)
os.Exit(1)
}
_, err = runCommand("nigiri", "faucet", "--liquid", receive.Onchain)
if err != nil {
fmt.Printf("error funding ark account: %s", err)
os.Exit(1)
}
time.Sleep(5 * time.Second)
code := m.Run()
_, err = runCommand("docker-compose", "-f", composePath, "down")
if err != nil {
fmt.Printf("error stopping docker-compose: %s", err)
os.Exit(1)
}
os.Exit(code)
}
func TestOnboard(t *testing.T) {
var balance arkBalance
balanceStr, err := runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
balanceBefore := balance.Offchain.Total
_, err = runArkCommand("onboard", "--amount", "1000", "--password", password)
require.NoError(t, err)
err = generateBlock()
require.NoError(t, err)
balanceStr, err = runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.Equal(t, balanceBefore+1000, balance.Offchain.Total)
}
func TestSendOffchain(t *testing.T) {
_, err := runArkCommand("onboard", "--amount", "1000", "--password", password)
require.NoError(t, err)
err = generateBlock()
require.NoError(t, err)
var receive arkReceive
receiveStr, err := runArkCommand("receive")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(receiveStr), &receive))
_, err = runArkCommand("send", "--amount", "1000", "--to", receive.Offchain, "--password", password)
require.NoError(t, err)
var balance arkBalance
balanceStr, err := runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.NotZero(t, balance.Offchain.Total)
}
func TestUnilateralExit(t *testing.T) {
_, err := runArkCommand("onboard", "--amount", "1000", "--password", password)
require.NoError(t, err)
err = generateBlock()
require.NoError(t, err)
var balance arkBalance
balanceStr, err := runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.NotZero(t, balance.Offchain.Total)
require.Len(t, balance.Onchain.Locked, 0)
_, err = runArkCommand("redeem", "--force", "--password", password)
require.NoError(t, err)
err = generateBlock()
require.NoError(t, err)
balanceStr, err = runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.Zero(t, balance.Offchain.Total)
require.Len(t, balance.Onchain.Locked, 1)
lockedBalance := balance.Onchain.Locked[0].Amount
require.NotZero(t, lockedBalance)
}
func TestCollaborativeExit(t *testing.T) {
_, err := runArkCommand("onboard", "--amount", "1000", "--password", password)
require.NoError(t, err)
err = generateBlock()
require.NoError(t, err)
var receive arkReceive
receiveStr, err := runArkCommand("receive")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(receiveStr), &receive))
var balance arkBalance
balanceStr, err := runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
balanceBefore := balance.Offchain.Total
balanceOnchainBefore := balance.Onchain.Spendable
_, err = runArkCommand("redeem", "--amount", "1000", "--address", receive.Onchain, "--password", password)
require.NoError(t, err)
time.Sleep(5 * time.Second)
balanceStr, err = runArkCommand("balance")
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.Equal(t, balanceBefore-1000, balance.Offchain.Total)
require.Equal(t, balanceOnchainBefore+1000, balance.Onchain.Spendable)
}

109
server/test/e2e/utils.go Normal file
View File

@@ -0,0 +1,109 @@
package e2e
import (
"fmt"
"io"
"os/exec"
"strings"
"sync"
"time"
)
const (
password = "password"
)
type arkBalance struct {
Offchain struct {
Total int `json:"total"`
} `json:"offchain_balance"`
Onchain struct {
Spendable int `json:"spendable_amount"`
Locked []struct {
Amount int `json:"amount"`
SpendableAt string `json:"spendable_at"`
} `json:"locked_amount"`
} `json:"onchain_balance"`
}
type arkReceive struct {
Offchain string `json:"offchain_address"`
Onchain string `json:"onchain_address"`
}
func runOceanCommand(arg ...string) (string, error) {
args := append([]string{"exec", "oceand", "ocean"}, arg...)
return runCommand("docker", args...)
}
func runArkCommand(arg ...string) (string, error) {
args := append([]string{"exec", "-t", "arkd", "ark"}, arg...)
return runCommand("docker", args...)
}
func generateBlock() error {
if _, err := runCommand("nigiri", "rpc", "--liquid", "generatetoaddress", "1", "el1qqwk722tghgkgmh3r2ph4d2apwj0dy9xnzlenzklx8jg3z299fpaw56trre9gpk6wmw0u4qycajqeva3t7lzp7wnacvwxha59r"); err != nil {
return err
}
time.Sleep(6 * time.Second)
return nil
}
func runCommand(name string, arg ...string) (string, error) {
errb := new(strings.Builder)
cmd := newCommand(name, arg...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
if err := cmd.Start(); err != nil {
return "", err
}
output := new(strings.Builder)
errorb := new(strings.Builder)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(output, stdout)
}()
go func() {
defer wg.Done()
io.Copy(errorb, stderr)
}()
wg.Wait()
if err := cmd.Wait(); err != nil {
if errMsg := errorb.String(); len(errMsg) > 0 {
return "", fmt.Errorf(errMsg)
}
if outMsg := output.String(); len(outMsg) > 0 {
return "", fmt.Errorf(outMsg)
}
return "", err
}
if errMsg := errb.String(); len(errMsg) > 0 {
return "", fmt.Errorf(errMsg)
}
return strings.Trim(output.String(), "\n"), nil
}
func newCommand(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
return cmd
}