Files
ark/client/redeem.go
Pietralberto Mazza 6d0d03e316 Cleanup (#121)
* Cleanup common

* Cleanup client

* Cleanup server

* Renamings

* Tidy up proto

* Update ocean protos

* Fixes

* Fixes
2024-02-28 18:05:03 +01:00

268 lines
5.5 KiB
Go

package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strings"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
)
var (
addressFlag = cli.StringFlag{
Name: "address",
Usage: "main chain address receiving the redeeemed VTXO",
Value: "",
Required: false,
}
amountToRedeemFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to redeem",
Value: 0,
Required: false,
}
forceFlag = cli.BoolFlag{
Name: "force",
Usage: "force redemption without collaborate with the Ark service provider",
Value: false,
Required: false,
}
)
var redeemCommand = cli.Command{
Name: "redeem",
Usage: "Redeem your offchain funds, either collaboratively or unilaterally",
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag},
Action: redeemAction,
}
func redeemAction(ctx *cli.Context) error {
addr := ctx.String("address")
amount := ctx.Uint64("amount")
force := ctx.Bool("force")
if len(addr) <= 0 && !force {
return fmt.Errorf("missing address flag (--address)")
}
if !force && amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
client, clean, err := getClientFromState()
if err != nil {
return err
}
defer clean()
if force {
if amount > 0 {
fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n")
}
return unilateralRedeem(client, ctx.Context)
}
return collaborativeRedeem(client, ctx.Context, addr, amount)
}
func collaborativeRedeem(
client arkv1.ArkServiceClient, ctx context.Context, addr string, amount uint64,
) error {
if _, err := address.ToOutputScript(addr); err != nil {
return fmt.Errorf("invalid onchain address")
}
net, err := address.NetworkForAddress(addr)
if err != nil {
return fmt.Errorf("invalid onchain address: unknown network")
}
_, liquidNet := getNetwork()
if net.Name != liquidNet.Name {
return fmt.Errorf("invalid onchain address: must be for %s network", liquidNet.Name)
}
if isConf, _ := address.IsConfidential(addr); isConf {
info, _ := address.FromConfidential(addr)
addr = info.Address
}
offchainAddr, _, _, err := getAddress()
if err != nil {
return err
}
receivers := []*arkv1.Output{
{
Address: addr,
Amount: amount,
},
}
explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, true)
if err != nil {
return err
}
selectedCoins, changeAmount, err := coinSelect(vtxos, amount)
if err != nil {
return err
}
if changeAmount > 0 {
receivers = append(receivers, &arkv1.Output{
Address: offchainAddr,
Amount: changeAmount,
})
}
inputs := make([]*arkv1.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
})
}
secKey, err := privateKeyFromPassword()
if err != nil {
return err
}
registerResponse, err := client.RegisterPayment(ctx, &arkv1.RegisterPaymentRequest{
Inputs: inputs,
})
if err != nil {
return err
}
_, err = client.ClaimPayment(ctx, &arkv1.ClaimPaymentRequest{
Id: registerResponse.GetId(),
Outputs: receivers,
})
if err != nil {
return err
}
poolTxID, err := handleRoundStream(
ctx,
client,
registerResponse.GetId(),
selectedCoins,
secKey,
receivers,
)
if err != nil {
return err
}
if err := printJSON(map[string]interface{}{
"pool_txid": poolTxID,
}); err != nil {
return err
}
return nil
}
func unilateralRedeem(client arkv1.ArkServiceClient, ctx context.Context) error {
offchainAddr, _, _, err := getAddress()
if err != nil {
return err
}
explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, false)
if err != nil {
return err
}
totalVtxosAmount := uint64(0)
for _, vtxo := range vtxos {
totalVtxosAmount += vtxo.amount
}
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)
if err != nil {
return err
}
for _, branch := range redeemBranches {
branchTxs, err := branch.redeemPath()
if err != nil {
return err
}
for _, txHex := range branchTxs {
if _, ok := transactionsMap[txHex]; !ok {
transactions = append(transactions, txHex)
transactionsMap[txHex] = struct{}{}
}
}
}
for i, txHex := range transactions {
for {
txid, err := explorer.Broadcast(txHex)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "bad-txns-inputs-missingorspent") {
time.Sleep(1 * time.Second)
} else {
return err
}
}
if len(txid) > 0 {
fmt.Printf("(%d/%d) broadcasted tx %s\n", i+1, len(transactions), txid)
break
}
}
}
return nil
}
// askForConfirmation asks the user for confirmation. A user must type in "yes" or "no" and then press enter.
// if the input is not recognized, it will ask again.
func askForConfirmation(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
return true
} else if response == "n" || response == "no" {
return false
}
}
}