mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 12:44:19 +01:00
CLI with ARK Sdk (#307)
* Export new methods * Use sdk in CLI * pr review refactor * go sync * Fixes * run integration test on every change * fixes * go sync * fix * Persist explorer url * Fix decoding bitcoin address * Add missing timeout to e2e test * Fix * Fixes --------- Co-authored-by: altafan <18440657+altafan@users.noreply.github.com>
This commit is contained in:
6
.github/workflows/ark.integration.yaml
vendored
6
.github/workflows/ark.integration.yaml
vendored
@@ -2,14 +2,10 @@ name: ci_integration
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "server/**"
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "server/**"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
|
||||
computeExpiryDetails := ctx.Bool(flags.ExpiryDetailsFlag.Name)
|
||||
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to check for error here becuase this function is called also by getAddress().
|
||||
// nolint:all
|
||||
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
chRes := make(chan balanceRes, 3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, amountByExpiration, err := getOffchainBalance(
|
||||
ctx, explorer, client, offchainAddr, computeExpiryDetails,
|
||||
)
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
chRes <- balanceRes{balance, 0, nil, amountByExpiration, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr, int64(timeoutBoarding))
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
|
||||
redemptionAddr, unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, err}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
lockedOnchainBalance := []map[string]interface{}{}
|
||||
details := make([]map[string]interface{}, 0)
|
||||
offchainBalance, onchainBalance := uint64(0), uint64(0)
|
||||
nextExpiration := int64(0)
|
||||
count := 0
|
||||
for res := range chRes {
|
||||
if res.err != nil {
|
||||
return res.err
|
||||
}
|
||||
if res.offchainBalance > 0 {
|
||||
offchainBalance = res.offchainBalance
|
||||
}
|
||||
if res.onchainSpendableBalance > 0 {
|
||||
onchainBalance += res.onchainSpendableBalance
|
||||
}
|
||||
if res.offchainBalanceByExpiration != nil {
|
||||
for timestamp, amount := range res.offchainBalanceByExpiration {
|
||||
if nextExpiration == 0 || timestamp < nextExpiration {
|
||||
nextExpiration = timestamp
|
||||
}
|
||||
|
||||
fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
|
||||
details = append(
|
||||
details,
|
||||
map[string]interface{}{
|
||||
"expiry_time": fancyTime,
|
||||
"amount": amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if res.onchainLockedBalance != nil {
|
||||
for timestamp, amount := range res.onchainLockedBalance {
|
||||
fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
|
||||
lockedOnchainBalance = append(
|
||||
lockedOnchainBalance,
|
||||
map[string]interface{}{
|
||||
"spendable_at": fancyTime,
|
||||
"amount": amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
count++
|
||||
if count == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
response["onchain_balance"] = map[string]interface{}{
|
||||
"spendable_amount": onchainBalance,
|
||||
}
|
||||
|
||||
if len(lockedOnchainBalance) > 0 {
|
||||
response["onchain_balance"].(map[string]interface{})["locked_amount"] = lockedOnchainBalance
|
||||
}
|
||||
|
||||
offchainBalanceJSON := map[string]interface{}{
|
||||
"total": offchainBalance,
|
||||
}
|
||||
|
||||
fancyTimeExpiration := ""
|
||||
if nextExpiration != 0 {
|
||||
t := time.Unix(nextExpiration, 0)
|
||||
if t.Before(time.Now().Add(48 * time.Hour)) {
|
||||
// print the duration instead of the absolute time
|
||||
until := time.Until(t)
|
||||
seconds := math.Abs(until.Seconds())
|
||||
minutes := math.Abs(until.Minutes())
|
||||
hours := math.Abs(until.Hours())
|
||||
|
||||
if hours < 1 {
|
||||
if minutes < 1 {
|
||||
fancyTimeExpiration = fmt.Sprintf("%d seconds", int(seconds))
|
||||
} else {
|
||||
fancyTimeExpiration = fmt.Sprintf("%d minutes", int(minutes))
|
||||
}
|
||||
} else {
|
||||
fancyTimeExpiration = fmt.Sprintf("%d hours", int(hours))
|
||||
}
|
||||
} else {
|
||||
fancyTimeExpiration = t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
offchainBalanceJSON["next_expiration"] = fancyTimeExpiration
|
||||
}
|
||||
|
||||
offchainBalanceJSON["details"] = details
|
||||
|
||||
response["offchain_balance"] = offchainBalanceJSON
|
||||
|
||||
return utils.PrintJSON(response)
|
||||
}
|
||||
|
||||
type balanceRes struct {
|
||||
offchainBalance uint64
|
||||
onchainSpendableBalance uint64
|
||||
onchainLockedBalance map[int64]uint64
|
||||
offchainBalanceByExpiration map[int64]uint64
|
||||
err error
|
||||
}
|
||||
|
||||
func getOffchainBalance(
|
||||
ctx *cli.Context, explorer utils.Explorer, client arkv1.ArkServiceClient,
|
||||
addr string, computeExpiration bool,
|
||||
) (uint64, map[int64]uint64, error) {
|
||||
amountByExpiration := make(map[int64]uint64, 0)
|
||||
|
||||
vtxos, err := getVtxos(ctx, explorer, client, addr, computeExpiration)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var balance uint64
|
||||
for _, vtxo := range vtxos {
|
||||
balance += vtxo.amount
|
||||
|
||||
if vtxo.expireAt != nil {
|
||||
expiration := vtxo.expireAt.Unix()
|
||||
|
||||
if _, ok := amountByExpiration[expiration]; !ok {
|
||||
amountByExpiration[expiration] = 0
|
||||
}
|
||||
|
||||
amountByExpiration[expiration] += vtxo.amount
|
||||
}
|
||||
}
|
||||
|
||||
return balance, amountByExpiration, nil
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *covenantLiquidCLI) Claim(ctx *cli.Context) error {
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue // cannot claim if onchain spendable
|
||||
}
|
||||
|
||||
boardingUtxos = append(boardingUtxos, u)
|
||||
}
|
||||
|
||||
var pendingBalance uint64
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
pendingBalance += utxo.Amount
|
||||
}
|
||||
|
||||
if pendingBalance == 0 {
|
||||
return fmt.Errorf("no boarding utxos to claim")
|
||||
}
|
||||
|
||||
receiver := receiver{
|
||||
To: offchainAddr,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
if len(ctx.String("password")) == 0 {
|
||||
if ok := askForConfirmation(
|
||||
fmt.Sprintf(
|
||||
"claim %d satoshis from %d boarding utxos",
|
||||
pendingBalance, len(boardingUtxos),
|
||||
),
|
||||
); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return selfTransferAllPendingPayments(
|
||||
ctx, client, boardingUtxos, receiver, boardingDescriptor,
|
||||
)
|
||||
}
|
||||
|
||||
func selfTransferAllPendingPayments(
|
||||
ctx *cli.Context,
|
||||
client arkv1.ArkServiceClient,
|
||||
boardingUtxos []utils.Utxo,
|
||||
myself receiver,
|
||||
desc string,
|
||||
) error {
|
||||
inputs := make([]*arkv1.Input, 0, len(boardingUtxos))
|
||||
|
||||
for _, outpoint := range boardingUtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: outpoint.Txid,
|
||||
Vout: outpoint.Vout,
|
||||
Descriptor_: desc,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
receiversOutput := []*arkv1.Output{
|
||||
{
|
||||
Address: myself.To,
|
||||
Amount: myself.Amount,
|
||||
},
|
||||
}
|
||||
|
||||
secKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registerResponse, err := client.RegisterPayment(
|
||||
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
|
||||
Id: registerResponse.GetId(),
|
||||
Outputs: []*arkv1.Output{{Address: myself.To, Amount: myself.Amount}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(), make([]vtxo, 0),
|
||||
len(boardingUtxos) > 0, secKey, receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"pool_txid": poolTxID,
|
||||
})
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/interfaces"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
type covenantLiquidCLI struct{}
|
||||
|
||||
func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"offchain_address": offchainAddr,
|
||||
"boarding_address": boardingAddr,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) Redeem(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(ctx)
|
||||
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(ctx, client)
|
||||
}
|
||||
|
||||
return collaborativeRedeem(ctx, client, addr, amount)
|
||||
}
|
||||
|
||||
func New() interfaces.CLI {
|
||||
return &covenantLiquidCLI{}
|
||||
}
|
||||
|
||||
type receiver struct {
|
||||
To string `json:"to"`
|
||||
Amount uint64 `json:"amount"`
|
||||
}
|
||||
|
||||
func (r *receiver) isOnchain() bool {
|
||||
_, err := address.ToOutputScript(r.To)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
pset, err := psetv2.New(nil, nil, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dust, err := utils.GetDust(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
targetAmount := uint64(0)
|
||||
for _, receiver := range receivers {
|
||||
targetAmount += receiver.Amount
|
||||
if receiver.Amount < dust {
|
||||
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, dust)
|
||||
}
|
||||
|
||||
script, err := address.ToOutputScript(receiver.To)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: liquidNet.AssetID,
|
||||
Amount: receiver.Amount,
|
||||
Script: script,
|
||||
},
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
utxos, change, err := coinSelectOnchain(
|
||||
ctx, explorer, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if change > 0 {
|
||||
_, changeAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeScript, err := address.ToOutputScript(changeAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: liquidNet.AssetID,
|
||||
Amount: change,
|
||||
Script: changeScript,
|
||||
},
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vBytes := utx.VirtualSize()
|
||||
feeAmount := uint64(math.Ceil(float64(vBytes) * 0.5))
|
||||
|
||||
if change > feeAmount {
|
||||
updater.Pset.Outputs[len(updater.Pset.Outputs)-1].Value = change - feeAmount
|
||||
} else if change == feeAmount {
|
||||
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
|
||||
} else { // change < feeAmount
|
||||
if change > 0 {
|
||||
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if newChange > 0 {
|
||||
_, changeAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeScript, err := address.ToOutputScript(changeAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: liquidNet.AssetID,
|
||||
Amount: newChange,
|
||||
Script: changeScript,
|
||||
},
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: liquidNet.AssetID,
|
||||
Amount: feeAmount,
|
||||
},
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
prvKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signPset(ctx, updater.Pset, explorer, prvKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := psetv2.FinalizeAll(updater.Pset); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return updater.Pset.ToBase64()
|
||||
}
|
||||
|
||||
func coinSelectOnchain(
|
||||
ctx *cli.Context,
|
||||
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
|
||||
) ([]utils.Utxo, uint64, error) {
|
||||
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]utils.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
now := time.Now()
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
|
||||
if utxo.SpendableAt.Before(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range redemptionUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
|
||||
|
||||
if utxo.SpendableAt.Before(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func addInputs(
|
||||
ctx *cli.Context,
|
||||
updater *psetv2.Updater,
|
||||
utxos []utils.Utxo,
|
||||
) error {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{
|
||||
{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
Sequence: sequence,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inputIndex := len(updater.Pset.Inputs) - 1
|
||||
|
||||
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func decodeReceiverAddress(addr string) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
) {
|
||||
outputScript, err := address.ToOutputScript(addr)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
}
|
||||
|
||||
return true, outputScript, nil, nil
|
||||
}
|
||||
|
||||
func getAddress(ctx *cli.Context) (offchainAddr, boardingAddr, redemptionAddr string, err error) {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkNet, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkAddr, err := common.EncodeAddress(arkNet.Addr, userPubkey, aspPubkey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(arkNet)
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
redemptionPay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
redemptionAddr, err = redemptionPay.TaprootAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(timeoutBoarding),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingPay, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingAddr, err = boardingPay.TaprootAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offchainAddr = arkAddr
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"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/psetv2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type vtxo struct {
|
||||
amount uint64
|
||||
txid string
|
||||
vout uint32
|
||||
poolTxid string
|
||||
expireAt *time.Time
|
||||
}
|
||||
|
||||
func getVtxos(
|
||||
ctx *cli.Context, explorer utils.Explorer, client arkv1.ArkServiceClient,
|
||||
addr string, computeExpiration bool,
|
||||
) ([]vtxo, error) {
|
||||
response, err := client.ListVtxos(ctx.Context, &arkv1.ListVtxosRequest{
|
||||
Address: addr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxos := make([]vtxo, 0, len(response.GetSpendableVtxos()))
|
||||
for _, v := range response.GetSpendableVtxos() {
|
||||
var expireAt *time.Time
|
||||
if v.ExpireAt > 0 {
|
||||
t := time.Unix(v.ExpireAt, 0)
|
||||
expireAt = &t
|
||||
}
|
||||
if v.Swept {
|
||||
continue
|
||||
}
|
||||
|
||||
if v.Outpoint.GetVtxoInput() != nil {
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.GetVtxoInput().GetTxid(),
|
||||
vout: v.Outpoint.GetVtxoInput().GetVout(),
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !computeExpiration {
|
||||
return vtxos, nil
|
||||
}
|
||||
|
||||
redeemBranches, err := getRedeemBranches(ctx.Context, explorer, client, vtxos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for vtxoTxid, branch := range redeemBranches {
|
||||
expiration, err := branch.expireAt(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, vtxo := range vtxos {
|
||||
if vtxo.txid == vtxoTxid {
|
||||
vtxos[i].expireAt = expiration
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vtxos, nil
|
||||
}
|
||||
|
||||
func getClientFromState(ctx *cli.Context) (arkv1.ArkServiceClient, func(), error) {
|
||||
state, err := utils.GetState(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
addr := state[utils.ASP_URL]
|
||||
if len(addr) <= 0 {
|
||||
return nil, nil, fmt.Errorf("missing asp url")
|
||||
}
|
||||
return getClient(addr)
|
||||
}
|
||||
|
||||
func getClient(addr string) (arkv1.ArkServiceClient, func(), error) {
|
||||
creds := insecure.NewCredentials()
|
||||
port := 80
|
||||
addr = strings.TrimPrefix(addr, "http://")
|
||||
if strings.HasPrefix(addr, "https://") {
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
creds = credentials.NewTLS(nil)
|
||||
port = 443
|
||||
}
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr = fmt.Sprintf("%s:%d", addr, port)
|
||||
}
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
client := arkv1.NewArkServiceClient(conn)
|
||||
|
||||
closeFn := func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("error closing connection: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return client, closeFn, nil
|
||||
}
|
||||
|
||||
func getRedeemBranches(
|
||||
ctx context.Context, explorer utils.Explorer, client arkv1.ArkServiceClient,
|
||||
vtxos []vtxo,
|
||||
) (map[string]*redeemBranch, error) {
|
||||
congestionTrees := make(map[string]tree.CongestionTree, 0)
|
||||
redeemBranches := make(map[string]*redeemBranch, 0)
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
|
||||
round, err := client.GetRound(ctx, &arkv1.GetRoundRequest{
|
||||
Txid: vtxo.poolTxid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
treeFromRound := round.GetRound().GetCongestionTree()
|
||||
congestionTree, err := toCongestionTree(treeFromRound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
congestionTrees[vtxo.poolTxid] = congestionTree
|
||||
}
|
||||
|
||||
redeemBranch, err := newRedeemBranch(
|
||||
explorer, congestionTrees[vtxo.poolTxid], vtxo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
redeemBranches[vtxo.txid] = redeemBranch
|
||||
}
|
||||
|
||||
return redeemBranches, nil
|
||||
}
|
||||
|
||||
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
|
||||
|
||||
for _, level := range treeFromProto.Levels {
|
||||
nodes := make([]tree.Node, 0, len(level.Nodes))
|
||||
|
||||
for _, node := range level.Nodes {
|
||||
nodes = append(nodes, tree.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
Leaf: false,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, nodes)
|
||||
}
|
||||
|
||||
for j, treeLvl := range levels {
|
||||
for i, node := range treeLvl {
|
||||
if len(levels.Children(node.Txid)) == 0 {
|
||||
levels[j][i].Leaf = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
func handleRoundStream(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
|
||||
vtxosToSign []vtxo, mustSignRoundTx bool,
|
||||
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
) (poolTxID string, err error) {
|
||||
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var pingStop func()
|
||||
pingReq := &arkv1.PingRequest{
|
||||
PaymentId: paymentID,
|
||||
}
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
|
||||
defer pingStop()
|
||||
|
||||
for {
|
||||
event, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if e := event.GetRoundFailed(); e != nil {
|
||||
pingStop()
|
||||
return "", fmt.Errorf("round failed: %s", e.GetReason())
|
||||
}
|
||||
|
||||
if e := event.GetRoundFinalization(); e != nil {
|
||||
// stop pinging as soon as we receive some forfeit txs
|
||||
pingStop()
|
||||
fmt.Println("round finalization started")
|
||||
|
||||
roundTx := e.GetPoolTx()
|
||||
ptx, err := psetv2.NewPsetFromBase64(roundTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
congestionTree, err := toCongestionTree(e.GetCongestionTree())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
connectors := e.GetConnectors()
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
roundLifetime, err := utils.GetRoundLifetime(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !isOnchainOnly(receivers) {
|
||||
// validate the congestion tree
|
||||
if err := tree.ValidateCongestionTree(
|
||||
congestionTree, roundTx, aspPubkey, int64(roundLifetime),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if err := common.ValidateConnectors(roundTx, connectors); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := decodeReceiverAddress(
|
||||
receiver.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if isOnChain {
|
||||
// collaborative exit case
|
||||
// search for the output in the pool tx
|
||||
found := false
|
||||
for _, output := range ptx.Outputs {
|
||||
if bytes.Equal(output.Script, onchainScript) {
|
||||
if output.Value != receiver.Amount {
|
||||
return "", fmt.Errorf(
|
||||
"invalid collaborative exit output amount: got %d, want %d",
|
||||
output.Value, receiver.Amount,
|
||||
)
|
||||
}
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", fmt.Errorf(
|
||||
"collaborative exit output not found: %s", receiver.Address,
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// off-chain send case
|
||||
// search for the output in congestion tree
|
||||
found := false
|
||||
|
||||
// compute the receiver output taproot key
|
||||
outputTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
leaves := congestionTree.Leaves()
|
||||
for _, leaf := range leaves {
|
||||
tx, err := psetv2.NewPsetFromBase64(leaf.Tx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, output := range tx.Outputs {
|
||||
if len(output.Script) == 0 {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(
|
||||
output.Script[2:], schnorr.SerializePubKey(outputTapKey),
|
||||
) {
|
||||
if output.Value != receiver.Amount {
|
||||
continue
|
||||
}
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", fmt.Errorf(
|
||||
"off-chain send output not found: %s", receiver.Address,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("congestion tree validated")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
|
||||
|
||||
if len(vtxosToSign) > 0 {
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, _ := psetv2.NewPsetFromBase64(connector)
|
||||
utx, _ := p.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
pset, err := psetv2.NewPsetFromBase64(forfeit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, input := range pset.Inputs {
|
||||
inputTxid := chainhash.Hash(input.PreviousTxid).String()
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
|
||||
if err := signPset(ctx, pset, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedPset, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no forfeit txs have been signed, start pinging again and wait for the next round
|
||||
if len(signedForfeits) == 0 {
|
||||
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
ptx, err := psetv2.NewPsetFromBase64(roundTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signPset(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err := ptx.ToBase64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("round tx signed")
|
||||
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
|
||||
}
|
||||
|
||||
fmt.Print("finalizing payment... ")
|
||||
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Print("done.\n")
|
||||
fmt.Println("waiting for round finalization...")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if event.GetRoundFinalized() != nil {
|
||||
return event.GetRoundFinalized().GetPoolTxid(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("stream closed unexpectedly")
|
||||
}
|
||||
|
||||
// send 1 ping message every 5 seconds to signal to the ark service that we are still alive
|
||||
// returns a function that can be used to stop the pinging
|
||||
func ping(
|
||||
ctx context.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest,
|
||||
) func() {
|
||||
_, err := client.Ping(ctx, req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
go func(t *time.Ticker) {
|
||||
for range t.C {
|
||||
// nolint
|
||||
client.Ping(ctx, req)
|
||||
}
|
||||
}(ticker)
|
||||
|
||||
return ticker.Stop
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"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 explorerUrls = map[string]string{
|
||||
common.Liquid.Name: "https://blockstream.info/liquid/api",
|
||||
common.LiquidTestNet.Name: "https://blockstream.info/liquidtestnet/api",
|
||||
common.LiquidRegTest.Name: "http://localhost:3001",
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) Init(ctx *cli.Context) error {
|
||||
key := ctx.String("prvkey")
|
||||
net := strings.ToLower(ctx.String("network"))
|
||||
url := ctx.String("asp-url")
|
||||
explorer := ctx.String("explorer")
|
||||
|
||||
var explorerURL string
|
||||
|
||||
if len(url) <= 0 {
|
||||
return fmt.Errorf("invalid asp-url")
|
||||
}
|
||||
if net != common.Liquid.Name && net != common.LiquidTestNet.Name && net != common.LiquidRegTest.Name {
|
||||
return fmt.Errorf("invalid network")
|
||||
}
|
||||
|
||||
if len(explorer) > 0 {
|
||||
explorerURL = explorer
|
||||
if err := testEsploraEndpoint(toElementsNetworkFromName(net), explorerURL); err != nil {
|
||||
return fmt.Errorf("failed to connect with explorer: %s", err)
|
||||
}
|
||||
} else {
|
||||
explorerURL = explorerUrls[net]
|
||||
}
|
||||
|
||||
if err := connectToAsp(ctx, net, url, explorerURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := utils.ReadPassword(ctx, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return initWallet(ctx, key, password)
|
||||
}
|
||||
|
||||
func generateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
|
||||
privKey, err := btcec.NewPrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func testEsploraEndpoint(net network.Network, url string) error {
|
||||
endpoint := fmt.Sprintf("%s/asset/%s", url, net.AssetID)
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect with explorer: (%s) %s", endpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf(endpoint + " " + string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
|
||||
client, close, err := getClient(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
resp, err := client.GetInfo(ctx.Context, &arkv1.GetInfoRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.SetState(ctx, map[string]string{
|
||||
utils.ASP_URL: url,
|
||||
utils.NETWORK: net,
|
||||
utils.ASP_PUBKEY: resp.Pubkey,
|
||||
utils.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
|
||||
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
|
||||
utils.EXPLORER: explorer,
|
||||
utils.DUST: strconv.Itoa(int(resp.GetDust())),
|
||||
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
|
||||
})
|
||||
}
|
||||
|
||||
func initWallet(ctx *cli.Context, key string, password []byte) error {
|
||||
var privateKey *secp256k1.PrivateKey
|
||||
if len(key) <= 0 {
|
||||
privKey, err := generateRandomPrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey = privKey
|
||||
} else {
|
||||
privKeyBytes, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes)
|
||||
}
|
||||
|
||||
cypher := utils.NewAES128Cypher()
|
||||
buf := privateKey.Serialize()
|
||||
encryptedPrivateKey, err := cypher.Encrypt(buf, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwordHash := utils.HashPassword([]byte(password))
|
||||
|
||||
pubkey := privateKey.PubKey().SerializeCompressed()
|
||||
state := map[string]string{
|
||||
utils.ENCRYPTED_PRVKEY: hex.EncodeToString(encryptedPrivateKey),
|
||||
utils.PASSWORD_HASH: hex.EncodeToString(passwordHash),
|
||||
utils.PUBKEY: hex.EncodeToString(pubkey),
|
||||
}
|
||||
|
||||
if err := utils.SetState(ctx, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("wallet initialized")
|
||||
return nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
)
|
||||
|
||||
func toElementsNetworkFromName(name string) network.Network {
|
||||
switch name {
|
||||
case common.Liquid.Name:
|
||||
return network.Liquid
|
||||
case common.LiquidTestNet.Name:
|
||||
return network.Testnet
|
||||
case common.LiquidRegTest.Name:
|
||||
return network.Regtest
|
||||
default:
|
||||
fmt.Printf("unknown network")
|
||||
return network.Liquid
|
||||
}
|
||||
}
|
||||
|
||||
func toElementsNetwork(net *common.Network) network.Network {
|
||||
return toElementsNetworkFromName(net.Name)
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
)
|
||||
|
||||
func collaborativeRedeem(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, addr string, amount uint64,
|
||||
) error {
|
||||
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
||||
|
||||
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")
|
||||
}
|
||||
netinstate, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dust, err := utils.GetDust(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(netinstate)
|
||||
|
||||
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(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
receivers := []*arkv1.Output{
|
||||
{
|
||||
Address: addr,
|
||||
Amount: amount,
|
||||
},
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, withExpiryCoinselect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedCoins, changeAmount, err := coinSelect(vtxos, amount, withExpiryCoinselect, dust)
|
||||
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{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
secKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registerResponse, err := client.RegisterPayment(ctx.Context, &arkv1.RegisterPaymentRequest{
|
||||
Inputs: inputs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
|
||||
Id: registerResponse.GetId(),
|
||||
Outputs: receivers,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx,
|
||||
client,
|
||||
registerResponse.GetId(),
|
||||
selectedCoins,
|
||||
false,
|
||||
secKey,
|
||||
receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"pool_txid": poolTxID,
|
||||
})
|
||||
}
|
||||
|
||||
func unilateralRedeem(ctx *cli.Context, client arkv1.ArkServiceClient) error {
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalVtxosAmount := uint64(0)
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
totalVtxosAmount += vtxo.amount
|
||||
}
|
||||
|
||||
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.Context, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
type redeemBranch struct {
|
||||
vtxo *vtxo
|
||||
branch []*psetv2.Pset
|
||||
internalKey *secp256k1.PublicKey
|
||||
sweepClosure *taproot.TapElementsLeaf
|
||||
lifetime time.Duration
|
||||
explorer utils.Explorer
|
||||
}
|
||||
|
||||
func newRedeemBranch(
|
||||
explorer utils.Explorer,
|
||||
congestionTree tree.CongestionTree, vtxo vtxo,
|
||||
) (*redeemBranch, error) {
|
||||
sweepClosure, seconds, err := findSweepClosure(congestionTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes, err := congestionTree.Branch(vtxo.txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branch := make([]*psetv2.Pset, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
branch = append(branch, pset)
|
||||
}
|
||||
|
||||
xOnlyKey := branch[0].Inputs[0].TapInternalKey
|
||||
internalKey, err := schnorr.ParsePubKey(xOnlyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &redeemBranch{
|
||||
vtxo: &vtxo,
|
||||
branch: branch,
|
||||
internalKey: internalKey,
|
||||
sweepClosure: sweepClosure,
|
||||
lifetime: lifetime,
|
||||
explorer: explorer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
|
||||
func (r *redeemBranch) redeemPath() ([]string, error) {
|
||||
transactions := make([]string, 0, len(r.branch))
|
||||
|
||||
offchainPath, err := r.offchainPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pset := range offchainPath {
|
||||
for i, input := range pset.Inputs {
|
||||
if len(input.TapLeafScript) == 0 {
|
||||
return nil, fmt.Errorf("tap leaf script not found on input #%d", i)
|
||||
}
|
||||
|
||||
for _, leaf := range input.TapLeafScript {
|
||||
closure, err := tree.DecodeClosure(leaf.Script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch closure.(type) {
|
||||
case *tree.UnrollClosure:
|
||||
controlBlock, err := leaf.ControlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTx.Inputs[i].Witness = [][]byte{
|
||||
leaf.Script,
|
||||
controlBlock[:],
|
||||
}
|
||||
|
||||
hex, err := unsignedTx.ToHex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transactions = append(transactions, hex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func (r *redeemBranch) expireAt(*cli.Context) (*time.Time, error) {
|
||||
lastKnownBlocktime := int64(0)
|
||||
|
||||
confirmed, blocktime, _ := r.explorer.GetTxBlocktime(r.vtxo.poolTxid)
|
||||
|
||||
if confirmed {
|
||||
lastKnownBlocktime = blocktime
|
||||
} else {
|
||||
expirationFromNow := time.Now().Add(time.Minute).Add(r.lifetime)
|
||||
return &expirationFromNow, nil
|
||||
}
|
||||
|
||||
for _, pset := range r.branch {
|
||||
utx, _ := pset.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
confirmed, blocktime, err := r.explorer.GetTxBlocktime(txid)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if confirmed {
|
||||
lastKnownBlocktime = blocktime
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
t := time.Unix(lastKnownBlocktime, 0).Add(r.lifetime)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// offchainPath checks for transactions of the branch onchain and returns only the offchain part
|
||||
func (r *redeemBranch) offchainPath() ([]*psetv2.Pset, error) {
|
||||
offchainPath := append([]*psetv2.Pset{}, r.branch...)
|
||||
|
||||
for i := len(r.branch) - 1; i >= 0; i-- {
|
||||
pset := r.branch[i]
|
||||
unsignedTx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txHash := unsignedTx.TxHash().String()
|
||||
|
||||
_, err = r.explorer.GetTxHex(txHash)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch
|
||||
if i == len(r.branch)-1 {
|
||||
offchainPath = []*psetv2.Pset{}
|
||||
} else {
|
||||
offchainPath = r.branch[i+1:]
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return offchainPath, nil
|
||||
}
|
||||
|
||||
func findSweepClosure(
|
||||
congestionTree tree.CongestionTree,
|
||||
) (*taproot.TapElementsLeaf, uint, error) {
|
||||
root, err := congestionTree.Root()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// find the sweep closure
|
||||
tx, err := psetv2.NewPsetFromBase64(root.Tx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var seconds uint
|
||||
var sweepClosure *taproot.TapElementsLeaf
|
||||
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
|
||||
closure := &tree.CSVSigClosure{}
|
||||
valid, err := closure.Decode(tapLeaf.Script)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if valid && closure.Seconds > seconds {
|
||||
seconds = closure.Seconds
|
||||
sweepClosure = &tapLeaf.TapElementsLeaf
|
||||
}
|
||||
}
|
||||
|
||||
if sweepClosure == nil {
|
||||
return nil, 0, fmt.Errorf("sweep closure not found")
|
||||
}
|
||||
|
||||
return sweepClosure, seconds, nil
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *covenantLiquidCLI) Send(ctx *cli.Context) error {
|
||||
if !ctx.IsSet("receivers") && !ctx.IsSet("to") && !ctx.IsSet("amount") {
|
||||
return fmt.Errorf("missing destination, either use --to and --amount to send or --receivers to send to many")
|
||||
}
|
||||
receivers := ctx.String("receivers")
|
||||
to := ctx.String("to")
|
||||
amount := ctx.Uint64("amount")
|
||||
|
||||
var receiversJSON []receiver
|
||||
if len(receivers) > 0 {
|
||||
if err := json.Unmarshal([]byte(receivers), &receiversJSON); err != nil {
|
||||
return fmt.Errorf("invalid receivers: %s", err)
|
||||
}
|
||||
} else {
|
||||
receiversJSON = []receiver{
|
||||
{
|
||||
To: to,
|
||||
Amount: amount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(receiversJSON) <= 0 {
|
||||
return fmt.Errorf("no receivers specified")
|
||||
}
|
||||
|
||||
onchainReceivers := make([]receiver, 0)
|
||||
offchainReceivers := make([]receiver, 0)
|
||||
|
||||
for _, receiver := range receiversJSON {
|
||||
if receiver.isOnchain() {
|
||||
onchainReceivers = append(onchainReceivers, receiver)
|
||||
} else {
|
||||
offchainReceivers = append(offchainReceivers, receiver)
|
||||
}
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
if len(onchainReceivers) > 0 {
|
||||
pset, err := sendOnchain(ctx, onchainReceivers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txid, err := explorer.Broadcast(pset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
}
|
||||
|
||||
if len(offchainReceivers) > 0 {
|
||||
if err := sendOffchain(ctx, offchainReceivers); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
||||
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
||||
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, aspPubKey, err := common.DecodeAddress(offchainAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dust, err := utils.GetDust(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
receiversOutput := make([]*arkv1.Output, 0)
|
||||
sumOfReceivers := uint64(0)
|
||||
|
||||
for _, receiver := range receivers {
|
||||
_, _, aspKey, err := common.DecodeAddress(receiver.To)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid receiver address: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(
|
||||
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
|
||||
) {
|
||||
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To)
|
||||
}
|
||||
|
||||
if receiver.Amount < dust {
|
||||
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, dust)
|
||||
}
|
||||
|
||||
receiversOutput = append(receiversOutput, &arkv1.Output{
|
||||
Address: receiver.To,
|
||||
Amount: uint64(receiver.Amount),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount
|
||||
}
|
||||
client, close, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, withExpiryCoinselect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selectedCoins, changeAmount, err := coinSelect(vtxos, sumOfReceivers, withExpiryCoinselect, dust)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if changeAmount > 0 {
|
||||
changeReceiver := &arkv1.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]*arkv1.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
secKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registerResponse, err := client.RegisterPayment(
|
||||
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
|
||||
Id: registerResponse.GetId(),
|
||||
Outputs: receiversOutput,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(),
|
||||
selectedCoins, false, secKey, receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"pool_txid": poolTxID,
|
||||
})
|
||||
}
|
||||
|
||||
func coinSelect(vtxos []vtxo, amount uint64, sortByExpirationTime bool, dust uint64) ([]vtxo, uint64, error) {
|
||||
selected := make([]vtxo, 0)
|
||||
notSelected := make([]vtxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
|
||||
if sortByExpirationTime {
|
||||
// sort vtxos by expiration (older first)
|
||||
sort.SliceStable(vtxos, func(i, j int) bool {
|
||||
if vtxos[i].expireAt == nil || vtxos[j].expireAt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return vtxos[i].expireAt.Before(*vtxos[j].expireAt)
|
||||
})
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
if selectedAmount >= amount {
|
||||
notSelected = append(notSelected, vtxo)
|
||||
break
|
||||
}
|
||||
|
||||
selected = append(selected, vtxo)
|
||||
selectedAmount += vtxo.amount
|
||||
}
|
||||
|
||||
if selectedAmount < amount {
|
||||
return nil, 0, fmt.Errorf("not enough funds to cover amount %d", amount)
|
||||
}
|
||||
|
||||
change := selectedAmount - amount
|
||||
|
||||
if change < dust {
|
||||
if len(notSelected) > 0 {
|
||||
selected = append(selected, notSelected[0])
|
||||
change += notSelected[0].amount
|
||||
}
|
||||
}
|
||||
|
||||
return selected, change, nil
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
func signPset(
|
||||
ctx *cli.Context, pset *psetv2.Pset, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
|
||||
) error {
|
||||
updater, err := psetv2.NewUpdater(pset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
if input.WitnessUtxo != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
prevoutTxHex, err := explorer.GetTxHex(chainhash.Hash(input.PreviousTxid).String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prevoutTx, err := transaction.NewTxFromHex(prevoutTxHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utxo := prevoutTx.Outputs[input.PreviousTxIndex]
|
||||
if utxo == nil {
|
||||
return fmt.Errorf("witness utxo not found")
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(i, utxo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := psetv2.NewSigner(updater.Pset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prevoutsScripts := make([][]byte, 0)
|
||||
prevoutsValues := make([][]byte, 0)
|
||||
prevoutsAssets := make([][]byte, 0)
|
||||
|
||||
for _, input := range pset.Inputs {
|
||||
prevoutsScripts = append(prevoutsScripts, input.WitnessUtxo.Script)
|
||||
prevoutsValues = append(prevoutsValues, input.WitnessUtxo.Value)
|
||||
prevoutsAssets = append(prevoutsAssets, input.WitnessUtxo.Asset)
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
if len(input.TapLeafScript) > 0 {
|
||||
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubkey := prvKey.PubKey()
|
||||
for _, leaf := range input.TapLeafScript {
|
||||
closure, err := tree.DecodeClosure(leaf.Script)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sign := false
|
||||
|
||||
switch c := closure.(type) {
|
||||
case *tree.CSVSigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
|
||||
case *tree.ForfeitClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
|
||||
}
|
||||
|
||||
if sign {
|
||||
hash := leaf.TapHash()
|
||||
|
||||
preimage := utx.HashForWitnessV1(
|
||||
i,
|
||||
prevoutsScripts,
|
||||
prevoutsAssets,
|
||||
prevoutsValues,
|
||||
txscript.SigHashDefault,
|
||||
genesis,
|
||||
&hash,
|
||||
nil,
|
||||
)
|
||||
|
||||
sig, err := schnorr.Sign(
|
||||
prvKey,
|
||||
preimage[:],
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tapScriptSig := psetv2.TapScriptSig{
|
||||
PartialSig: psetv2.PartialSig{
|
||||
PubKey: schnorr.SerializePubKey(prvKey.PubKey()),
|
||||
Signature: sig.Serialize(),
|
||||
},
|
||||
LeafHash: hash.CloneBytes(),
|
||||
}
|
||||
|
||||
if err := signer.SignTaprootInputTapscriptSig(i, tapScriptSig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
if len(input.PartialSigs) > 0 {
|
||||
valid, err := pset.ValidateInputSignatures(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid signature for input %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
func computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint,
|
||||
) (*secp256k1.PublicKey, *taproot.TapscriptElementsProof, error) {
|
||||
redeemClosure := &tree.CSVSigClosure{
|
||||
Pubkey: userPubkey,
|
||||
Seconds: exitDelay,
|
||||
}
|
||||
|
||||
forfeitClosure := &tree.ForfeitClosure{
|
||||
Pubkey: userPubkey,
|
||||
AspPubkey: aspPubkey,
|
||||
}
|
||||
|
||||
redeemLeaf, err := redeemClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
vtxoTaprootTree := taproot.AssembleTaprootScriptTree(
|
||||
*redeemLeaf, *forfeitLeaf,
|
||||
)
|
||||
root := vtxoTaprootTree.RootNode.TapHash()
|
||||
|
||||
unspendableKey := tree.UnspendableKey()
|
||||
vtxoTaprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:])
|
||||
|
||||
redeemLeafHash := redeemLeaf.TapHash()
|
||||
proofIndex := vtxoTaprootTree.LeafProofIndex[redeemLeafHash]
|
||||
proof := vtxoTaprootTree.LeafMerkleProofs[proofIndex]
|
||||
|
||||
return vtxoTaprootKey, &proof, nil
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
computeExpiryDetails := ctx.Bool(flags.ExpiryDetailsFlag.Name)
|
||||
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// No need to check for error here becuase this function is called also by getAddress().
|
||||
// nolint:all
|
||||
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
chRes := make(chan balanceRes, 3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, amountByExpiration, err := getOffchainBalance(
|
||||
ctx, explorer, client, offchainAddr, computeExpiryDetails,
|
||||
)
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
chRes <- balanceRes{balance, 0, nil, amountByExpiration, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr.EncodeAddress(), int64(timeoutBoarding))
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{0, balance, lockedBalance, nil, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
|
||||
redemptionAddr.EncodeAddress(), unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, err}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
lockedOnchainBalance := []map[string]interface{}{}
|
||||
details := make([]map[string]interface{}, 0)
|
||||
offchainBalance, onchainBalance := uint64(0), uint64(0)
|
||||
nextExpiration := int64(0)
|
||||
count := 0
|
||||
for res := range chRes {
|
||||
if res.err != nil {
|
||||
return res.err
|
||||
}
|
||||
if res.offchainBalance > 0 {
|
||||
offchainBalance = res.offchainBalance
|
||||
}
|
||||
if res.onchainSpendableBalance > 0 {
|
||||
onchainBalance += res.onchainSpendableBalance
|
||||
}
|
||||
if res.offchainBalanceByExpiration != nil {
|
||||
for timestamp, amount := range res.offchainBalanceByExpiration {
|
||||
if nextExpiration == 0 || timestamp < nextExpiration {
|
||||
nextExpiration = timestamp
|
||||
}
|
||||
|
||||
fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
|
||||
details = append(
|
||||
details,
|
||||
map[string]interface{}{
|
||||
"expiry_time": fancyTime,
|
||||
"amount": amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if res.onchainLockedBalance != nil {
|
||||
for timestamp, amount := range res.onchainLockedBalance {
|
||||
fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
|
||||
lockedOnchainBalance = append(
|
||||
lockedOnchainBalance,
|
||||
map[string]interface{}{
|
||||
"spendable_at": fancyTime,
|
||||
"amount": amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
count++
|
||||
if count == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
|
||||
response["onchain_balance"] = map[string]interface{}{
|
||||
"spendable_amount": onchainBalance,
|
||||
}
|
||||
|
||||
if len(lockedOnchainBalance) > 0 {
|
||||
response["onchain_balance"].(map[string]interface{})["locked_amount"] = lockedOnchainBalance
|
||||
}
|
||||
|
||||
offchainBalanceJSON := map[string]interface{}{
|
||||
"total": offchainBalance,
|
||||
}
|
||||
|
||||
fancyTimeExpiration := ""
|
||||
if nextExpiration != 0 {
|
||||
t := time.Unix(nextExpiration, 0)
|
||||
if t.Before(time.Now().Add(48 * time.Hour)) {
|
||||
// print the duration instead of the absolute time
|
||||
until := time.Until(t)
|
||||
seconds := math.Abs(until.Seconds())
|
||||
minutes := math.Abs(until.Minutes())
|
||||
hours := math.Abs(until.Hours())
|
||||
|
||||
if hours < 1 {
|
||||
if minutes < 1 {
|
||||
fancyTimeExpiration = fmt.Sprintf("%d seconds", int(seconds))
|
||||
} else {
|
||||
fancyTimeExpiration = fmt.Sprintf("%d minutes", int(minutes))
|
||||
}
|
||||
} else {
|
||||
fancyTimeExpiration = fmt.Sprintf("%d hours", int(hours))
|
||||
}
|
||||
} else {
|
||||
fancyTimeExpiration = t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
offchainBalanceJSON["next_expiration"] = fancyTimeExpiration
|
||||
}
|
||||
|
||||
offchainBalanceJSON["details"] = details
|
||||
|
||||
response["offchain_balance"] = offchainBalanceJSON
|
||||
|
||||
return utils.PrintJSON(response)
|
||||
}
|
||||
|
||||
type balanceRes struct {
|
||||
offchainBalance uint64
|
||||
onchainSpendableBalance uint64
|
||||
onchainLockedBalance map[int64]uint64
|
||||
offchainBalanceByExpiration map[int64]uint64
|
||||
err error
|
||||
}
|
||||
|
||||
func getOffchainBalance(
|
||||
ctx *cli.Context, explorer utils.Explorer, client arkv1.ArkServiceClient,
|
||||
addr string, computeExpiration bool,
|
||||
) (uint64, map[int64]uint64, error) {
|
||||
amountByExpiration := make(map[int64]uint64, 0)
|
||||
|
||||
vtxos, err := getVtxos(ctx, explorer, client, addr, computeExpiration)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var balance uint64
|
||||
for _, vtxo := range vtxos {
|
||||
balance += vtxo.amount
|
||||
|
||||
if vtxo.expireAt != nil {
|
||||
expiration := vtxo.expireAt.Unix()
|
||||
|
||||
if _, ok := amountByExpiration[expiration]; !ok {
|
||||
amountByExpiration[expiration] = 0
|
||||
}
|
||||
|
||||
amountByExpiration[expiration] += vtxo.amount
|
||||
}
|
||||
}
|
||||
|
||||
return balance, amountByExpiration, nil
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) Claim(ctx *cli.Context) error {
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue // cannot claim if onchain spendable
|
||||
}
|
||||
|
||||
boardingUtxos = append(boardingUtxos, u)
|
||||
}
|
||||
|
||||
vtxos, err := getVtxos(ctx, nil, client, offchainAddr, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pendingBalance uint64
|
||||
var pendingVtxos []vtxo
|
||||
for _, vtxo := range vtxos {
|
||||
if vtxo.pending {
|
||||
pendingBalance += vtxo.amount
|
||||
pendingVtxos = append(pendingVtxos, vtxo)
|
||||
}
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
pendingBalance += utxo.Amount
|
||||
}
|
||||
|
||||
if pendingBalance == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
receiver := receiver{
|
||||
To: offchainAddr,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
if len(ctx.String("password")) == 0 {
|
||||
if ok := askForConfirmation(
|
||||
fmt.Sprintf(
|
||||
"claim %d satoshis from %d pending payments and %d boarding utxos",
|
||||
pendingBalance, len(pendingVtxos), len(boardingUtxos),
|
||||
),
|
||||
); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return selfTransferAllPendingPayments(
|
||||
ctx, client, pendingVtxos, boardingUtxos, receiver, boardingDescriptor,
|
||||
)
|
||||
}
|
||||
|
||||
func selfTransferAllPendingPayments(
|
||||
ctx *cli.Context,
|
||||
client arkv1.ArkServiceClient,
|
||||
pendingVtxos []vtxo,
|
||||
boardingUtxos []utils.Utxo,
|
||||
myself receiver,
|
||||
desc string,
|
||||
) error {
|
||||
inputs := make([]*arkv1.Input, 0, len(pendingVtxos)+len(boardingUtxos))
|
||||
|
||||
for _, coin := range pendingVtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(boardingUtxos) > 0 {
|
||||
for _, outpoint := range boardingUtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: outpoint.Txid,
|
||||
Vout: outpoint.Vout,
|
||||
Descriptor_: desc,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
receiversOutput := []*arkv1.Output{
|
||||
{
|
||||
Address: myself.To,
|
||||
Amount: myself.Amount,
|
||||
},
|
||||
}
|
||||
|
||||
secKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ephemeralKey, err := secp256k1.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubkey := hex.EncodeToString(ephemeralKey.PubKey().SerializeCompressed())
|
||||
|
||||
registerResponse, err := client.RegisterPayment(
|
||||
ctx.Context,
|
||||
&arkv1.RegisterPaymentRequest{
|
||||
Inputs: inputs,
|
||||
EphemeralPubkey: &pubkey,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
|
||||
Id: registerResponse.GetId(),
|
||||
Outputs: []*arkv1.Output{{Address: myself.To, Amount: myself.Amount}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(), pendingVtxos,
|
||||
len(boardingUtxos) > 0, secKey, receiversOutput, ephemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"pool_txid": poolTxID,
|
||||
})
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/client/interfaces"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type clArkBitcoinCLI struct{}
|
||||
|
||||
func (c *clArkBitcoinCLI) Receive(ctx *cli.Context) error {
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"offchain_address": offchainAddr,
|
||||
"boarding_address": boardingAddr.EncodeAddress(),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *clArkBitcoinCLI) Redeem(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(ctx)
|
||||
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(ctx, client)
|
||||
}
|
||||
|
||||
return collaborativeRedeem(ctx, client, addr, amount)
|
||||
}
|
||||
|
||||
func New() interfaces.CLI {
|
||||
return &clArkBitcoinCLI{}
|
||||
}
|
||||
|
||||
func (c *clArkBitcoinCLI) Send(ctx *cli.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
type receiver struct {
|
||||
To string `json:"to"`
|
||||
Amount uint64 `json:"amount"`
|
||||
}
|
||||
|
||||
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
ptx, err := psbt.New(nil, nil, 2, 0, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
updater, err := psbt.NewUpdater(ptx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dust, err := utils.GetDust(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
netParams := toChainParams(net)
|
||||
|
||||
targetAmount := uint64(0)
|
||||
for _, receiver := range receivers {
|
||||
targetAmount += receiver.Amount
|
||||
if receiver.Amount < dust {
|
||||
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, dust)
|
||||
}
|
||||
|
||||
rcvAddr, err := btcutil.DecodeAddress(receiver.To, &netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(rcvAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxOut(&wire.TxOut{
|
||||
Value: int64(receiver.Amount),
|
||||
PkScript: pkscript,
|
||||
})
|
||||
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
utxos, change, err := coinSelectOnchain(
|
||||
ctx, explorer, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if change > 0 {
|
||||
_, changeAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(changeAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxOut(&wire.TxOut{
|
||||
Value: int64(change),
|
||||
PkScript: pkscript,
|
||||
})
|
||||
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
|
||||
}
|
||||
|
||||
size := updater.Upsbt.UnsignedTx.SerializeSize()
|
||||
feeRate, err := explorer.GetFeeRate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
feeAmount := uint64(math.Ceil(float64(size)*feeRate) + 50)
|
||||
|
||||
if change > feeAmount {
|
||||
updater.Upsbt.UnsignedTx.TxOut[len(updater.Upsbt.Outputs)-1].Value = int64(change - feeAmount)
|
||||
} else if change == feeAmount {
|
||||
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
|
||||
} else { // change < feeAmount
|
||||
if change > 0 {
|
||||
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if newChange > 0 {
|
||||
_, changeAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(changeAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxOut(&wire.TxOut{
|
||||
Value: int64(newChange),
|
||||
PkScript: pkscript,
|
||||
})
|
||||
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
|
||||
}
|
||||
}
|
||||
|
||||
prvKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, updater.Upsbt, explorer, prvKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := range updater.Upsbt.Inputs {
|
||||
if err := psbt.Finalize(updater.Upsbt, i); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return updater.Upsbt.B64Encode()
|
||||
}
|
||||
|
||||
func coinSelectOnchain(
|
||||
ctx *cli.Context,
|
||||
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
|
||||
) ([]utils.Utxo, uint64, error) {
|
||||
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]utils.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
now := time.Now()
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
|
||||
if utxo.SpendableAt.After(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range redemptionUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
|
||||
|
||||
if utxo.SpendableAt.After(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func addInputs(
|
||||
ctx *cli.Context,
|
||||
updater *psbt.Updater,
|
||||
utxos []utils.Utxo,
|
||||
) error {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
},
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
_, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
|
||||
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: leafProof.LeafVersion,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeReceiverAddress(addr string) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
) {
|
||||
decoded, err := btcutil.DecodeAddress(addr, nil)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
}
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(decoded)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
return true, pkscript, nil, nil
|
||||
}
|
||||
|
||||
func getAddress(ctx *cli.Context) (offchainAddr string, boardingAddr, redemptionAddr btcutil.Address, err error) {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkNet, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkAddr, err := common.EncodeAddress(arkNet.Addr, userPubkey, aspPubkey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
netParams := toChainParams(arkNet)
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
redemptionP2TR, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(vtxoTapKey),
|
||||
&netParams,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(timeoutBoarding),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingP2TR, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(boardingTapKey),
|
||||
&netParams,
|
||||
)
|
||||
|
||||
redemptionAddr = redemptionP2TR
|
||||
boardingAddr = boardingP2TR
|
||||
offchainAddr = arkAddr
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,660 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type vtxo struct {
|
||||
amount uint64
|
||||
txid string
|
||||
vout uint32
|
||||
poolTxid string
|
||||
expireAt *time.Time
|
||||
pending bool
|
||||
}
|
||||
|
||||
func getVtxos(
|
||||
ctx *cli.Context, explorer utils.Explorer, client arkv1.ArkServiceClient,
|
||||
addr string, computeExpiration bool,
|
||||
) ([]vtxo, error) {
|
||||
response, err := client.ListVtxos(ctx.Context, &arkv1.ListVtxosRequest{
|
||||
Address: addr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxos := make([]vtxo, 0, len(response.GetSpendableVtxos()))
|
||||
for _, v := range response.GetSpendableVtxos() {
|
||||
var expireAt *time.Time
|
||||
if v.GetExpireAt() > 0 {
|
||||
t := time.Unix(v.ExpireAt, 0)
|
||||
expireAt = &t
|
||||
}
|
||||
if v.GetSwept() {
|
||||
continue
|
||||
}
|
||||
if v.Outpoint.GetVtxoInput() != nil {
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.GetVtxoInput().GetTxid(),
|
||||
vout: v.Outpoint.GetVtxoInput().GetVout(),
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
pending: v.GetPending(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !computeExpiration {
|
||||
return vtxos, nil
|
||||
}
|
||||
|
||||
redeemBranches, err := getRedeemBranches(ctx.Context, explorer, client, vtxos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for vtxoTxid, branch := range redeemBranches {
|
||||
expiration, err := branch.expireAt(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, vtxo := range vtxos {
|
||||
if vtxo.txid == vtxoTxid {
|
||||
vtxos[i].expireAt = expiration
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vtxos, nil
|
||||
}
|
||||
|
||||
func getClientFromState(ctx *cli.Context) (arkv1.ArkServiceClient, func(), error) {
|
||||
state, err := utils.GetState(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
addr := state[utils.ASP_URL]
|
||||
if len(addr) <= 0 {
|
||||
return nil, nil, fmt.Errorf("missing asp url")
|
||||
}
|
||||
return getClient(addr)
|
||||
}
|
||||
|
||||
func getClient(addr string) (arkv1.ArkServiceClient, func(), error) {
|
||||
creds := insecure.NewCredentials()
|
||||
port := 80
|
||||
addr = strings.TrimPrefix(addr, "http://")
|
||||
if strings.HasPrefix(addr, "https://") {
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
creds = credentials.NewTLS(nil)
|
||||
port = 443
|
||||
}
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr = fmt.Sprintf("%s:%d", addr, port)
|
||||
}
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
client := arkv1.NewArkServiceClient(conn)
|
||||
|
||||
closeFn := func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("error closing connection: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return client, closeFn, nil
|
||||
}
|
||||
|
||||
func getRedeemBranches(
|
||||
ctx context.Context, explorer utils.Explorer, client arkv1.ArkServiceClient,
|
||||
vtxos []vtxo,
|
||||
) (map[string]*redeemBranch, error) {
|
||||
congestionTrees := make(map[string]tree.CongestionTree, 0)
|
||||
redeemBranches := make(map[string]*redeemBranch, 0)
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
|
||||
round, err := client.GetRound(ctx, &arkv1.GetRoundRequest{
|
||||
Txid: vtxo.poolTxid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
treeFromRound := round.GetRound().GetCongestionTree()
|
||||
congestionTree, err := toCongestionTree(treeFromRound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
congestionTrees[vtxo.poolTxid] = congestionTree
|
||||
}
|
||||
|
||||
redeemBranch, err := newRedeemBranch(
|
||||
explorer, congestionTrees[vtxo.poolTxid], vtxo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
redeemBranches[vtxo.txid] = redeemBranch
|
||||
}
|
||||
|
||||
return redeemBranches, nil
|
||||
}
|
||||
|
||||
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
|
||||
|
||||
for _, level := range treeFromProto.Levels {
|
||||
nodes := make([]tree.Node, 0, len(level.Nodes))
|
||||
|
||||
for _, node := range level.Nodes {
|
||||
nodes = append(nodes, tree.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
Leaf: false,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, nodes)
|
||||
}
|
||||
|
||||
for j, treeLvl := range levels {
|
||||
for i, node := range treeLvl {
|
||||
if len(levels.Children(node.Txid)) == 0 {
|
||||
levels[j][i].Leaf = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
func handleRoundStream(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
|
||||
vtxosToSign []vtxo, mustSignRoundTx bool,
|
||||
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
ephemeralKey *secp256k1.PrivateKey,
|
||||
) (poolTxID string, err error) {
|
||||
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
myEphemeralPublicKey := hex.EncodeToString(ephemeralKey.PubKey().SerializeCompressed())
|
||||
|
||||
var pingStop func()
|
||||
pingReq := &arkv1.PingRequest{
|
||||
PaymentId: paymentID,
|
||||
}
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
|
||||
defer pingStop()
|
||||
|
||||
var treeSignerSession bitcointree.SignerSession
|
||||
|
||||
for {
|
||||
event, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if e := event.GetRoundFailed(); e != nil {
|
||||
pingStop()
|
||||
return "", fmt.Errorf("round failed: %s", e.GetReason())
|
||||
}
|
||||
|
||||
if e := event.GetRoundSigning(); e != nil {
|
||||
pingStop()
|
||||
fmt.Println("tree created, generating nonces...")
|
||||
|
||||
cosignersPubKeysHex := e.GetCosignersPubkeys()
|
||||
|
||||
cosigners := make([]*secp256k1.PublicKey, 0, len(cosignersPubKeysHex))
|
||||
|
||||
for _, pubkeyHex := range cosignersPubKeysHex {
|
||||
pubkeyBytes, err := hex.DecodeString(pubkeyHex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cosigners = append(cosigners, pubkey)
|
||||
}
|
||||
|
||||
congestionTree, err := toCongestionTree(e.GetUnsignedTree())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lifetime, err := utils.GetRoundLifetime(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sweepClosure := bitcointree.CSVSigClosure{
|
||||
Pubkey: aspPubkey,
|
||||
Seconds: uint(lifetime),
|
||||
}
|
||||
|
||||
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||
root := sweepTapTree.RootNode.TapHash()
|
||||
|
||||
roundTx, err := psbt.NewFromRawBytes(strings.NewReader(e.GetUnsignedRoundTx()), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sharedOutput := roundTx.UnsignedTx.TxOut[0]
|
||||
sharedOutputValue := sharedOutput.Value
|
||||
|
||||
treeSignerSession = bitcointree.NewTreeSignerSession(
|
||||
ephemeralKey, sharedOutputValue, congestionTree, root.CloneBytes(),
|
||||
)
|
||||
|
||||
nonces, err := treeSignerSession.GetNonces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := treeSignerSession.SetKeys(cosigners); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var nonceBuffer bytes.Buffer
|
||||
|
||||
if err := nonces.Encode(&nonceBuffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
serializedNonces := hex.EncodeToString(nonceBuffer.Bytes())
|
||||
|
||||
if _, err := client.SendTreeNonces(ctx.Context, &arkv1.SendTreeNoncesRequest{
|
||||
RoundId: e.GetId(),
|
||||
PublicKey: myEphemeralPublicKey,
|
||||
TreeNonces: serializedNonces,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("nonces sent")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if e := event.GetRoundSigningNoncesGenerated(); e != nil {
|
||||
pingStop()
|
||||
fmt.Println("nonces generated, signing the tree...")
|
||||
|
||||
if treeSignerSession == nil {
|
||||
return "", fmt.Errorf("tree signer session not set")
|
||||
}
|
||||
|
||||
combinedNoncesBytes, err := hex.DecodeString(e.GetTreeNonces())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
combinedNonces, err := bitcointree.DecodeNonces(bytes.NewReader(combinedNoncesBytes))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := treeSignerSession.SetAggregatedNonces(combinedNonces); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sigs, err := treeSignerSession.Sign()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var sigBuffer bytes.Buffer
|
||||
|
||||
if err := sigs.Encode(&sigBuffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
serializedSigs := hex.EncodeToString(sigBuffer.Bytes())
|
||||
|
||||
if _, err := client.SendTreeSignatures(ctx.Context, &arkv1.SendTreeSignaturesRequest{
|
||||
RoundId: e.GetId(),
|
||||
TreeSignatures: serializedSigs,
|
||||
PublicKey: myEphemeralPublicKey,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("tree signed")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if e := event.GetRoundFinalization(); e != nil {
|
||||
// stop pinging as soon as we receive some forfeit txs
|
||||
pingStop()
|
||||
|
||||
roundTx := e.GetPoolTx()
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
congestionTree, err := toCongestionTree(e.GetCongestionTree())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
connectors := e.GetConnectors()
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
roundLifetime, err := utils.GetRoundLifetime(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !isOnchainOnly(receivers) {
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
congestionTree, roundTx, aspPubkey, int64(roundLifetime),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO bitcoin validateConnectors
|
||||
// if err := common.ValidateConnectors(poolTx, connectors); err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := decodeReceiverAddress(
|
||||
receiver.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if isOnChain {
|
||||
// collaborative exit case
|
||||
// search for the output in the pool tx
|
||||
found := false
|
||||
for _, output := range ptx.UnsignedTx.TxOut {
|
||||
if bytes.Equal(output.PkScript, onchainScript) {
|
||||
if output.Value != int64(receiver.Amount) {
|
||||
return "", fmt.Errorf(
|
||||
"invalid collaborative exit output amount: got %d, want %d",
|
||||
output.Value, receiver.Amount,
|
||||
)
|
||||
}
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", fmt.Errorf(
|
||||
"collaborative exit output not found: %s", receiver.Address,
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// off-chain send case
|
||||
// search for the output in congestion tree
|
||||
found := false
|
||||
|
||||
// compute the receiver output taproot key
|
||||
outputTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
leaves := congestionTree.Leaves()
|
||||
for _, leaf := range leaves {
|
||||
tx, err := psbt.NewFromRawBytes(strings.NewReader(leaf.Tx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, output := range tx.UnsignedTx.TxOut {
|
||||
if len(output.PkScript) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(
|
||||
output.PkScript[2:], schnorr.SerializePubKey(outputTapKey),
|
||||
) {
|
||||
if output.Value != int64(receiver.Amount) {
|
||||
continue
|
||||
}
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", fmt.Errorf(
|
||||
"off-chain send output not found: %s", receiver.Address,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("congestion tree validated")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
|
||||
|
||||
if len(vtxosToSign) > 0 {
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
txid := p.UnsignedTx.TxHash().String()
|
||||
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, input := range ptx.UnsignedTx.TxIn {
|
||||
inputTxid := input.PreviousOutPoint.Hash.String()
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedPset, err := ptx.B64Encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no forfeit txs have been signed, start pinging again and wait for the next round
|
||||
if len(vtxosToSign) > 0 && len(signedForfeits) == 0 {
|
||||
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
pingStop = ping(ctx.Context, client, pingReq)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err := ptx.B64Encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("round tx signed")
|
||||
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
|
||||
}
|
||||
|
||||
fmt.Print("finalizing payment... ")
|
||||
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Print("done.\n")
|
||||
fmt.Println("waiting for round finalization...")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if event.GetRoundFinalized() != nil {
|
||||
return event.GetRoundFinalized().GetPoolTxid(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("stream closed unexpectedly")
|
||||
}
|
||||
|
||||
// send 1 ping message every 5 seconds to signal to the ark service that we are still alive
|
||||
// returns a function that can be used to stop the pinging
|
||||
func ping(
|
||||
ctx context.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest,
|
||||
) func() {
|
||||
_, err := client.Ping(ctx, req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
go func(t *time.Ticker) {
|
||||
for range t.C {
|
||||
// nolint
|
||||
client.Ping(ctx, req)
|
||||
}
|
||||
}(ticker)
|
||||
|
||||
return ticker.Stop
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var explorerUrls = map[string]string{
|
||||
common.Bitcoin.Name: "https://blockstream.info/api",
|
||||
common.BitcoinTestNet.Name: "https://blockstream.info/testnet/api",
|
||||
common.BitcoinRegTest.Name: "http://localhost:3000",
|
||||
common.BitcoinSigNet.Name: "https://mutinynet.com/api",
|
||||
}
|
||||
|
||||
func (c *clArkBitcoinCLI) Init(ctx *cli.Context) error {
|
||||
key := ctx.String("prvkey")
|
||||
net := strings.ToLower(ctx.String("network"))
|
||||
url := ctx.String("asp-url")
|
||||
explorer := ctx.String("explorer")
|
||||
|
||||
var explorerURL string
|
||||
|
||||
if len(url) <= 0 {
|
||||
return fmt.Errorf("invalid ark url")
|
||||
}
|
||||
if net != common.Bitcoin.Name && net != common.BitcoinTestNet.Name && net != common.BitcoinRegTest.Name && net != common.BitcoinSigNet.Name {
|
||||
return fmt.Errorf("invalid network")
|
||||
}
|
||||
|
||||
if len(explorer) > 0 {
|
||||
explorerURL = explorer
|
||||
} else {
|
||||
explorerURL = explorerUrls[net]
|
||||
}
|
||||
|
||||
if err := connectToAsp(ctx, net, url, explorerURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := utils.ReadPassword(ctx, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return initWallet(ctx, key, password)
|
||||
}
|
||||
|
||||
func generateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
|
||||
privKey, err := btcec.NewPrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
|
||||
client, close, err := getClient(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
resp, err := client.GetInfo(ctx.Context, &arkv1.GetInfoRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.SetState(ctx, map[string]string{
|
||||
utils.ASP_URL: url,
|
||||
utils.NETWORK: net,
|
||||
utils.ASP_PUBKEY: resp.Pubkey,
|
||||
utils.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
|
||||
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
|
||||
utils.EXPLORER: explorer,
|
||||
utils.DUST: strconv.Itoa(int(resp.GetDust())),
|
||||
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
|
||||
})
|
||||
}
|
||||
|
||||
func initWallet(ctx *cli.Context, key string, password []byte) error {
|
||||
var privateKey *secp256k1.PrivateKey
|
||||
if len(key) <= 0 {
|
||||
privKey, err := generateRandomPrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey = privKey
|
||||
} else {
|
||||
privKeyBytes, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes)
|
||||
}
|
||||
|
||||
cypher := utils.NewAES128Cypher()
|
||||
buf := privateKey.Serialize()
|
||||
encryptedPrivateKey, err := cypher.Encrypt(buf, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwordHash := utils.HashPassword([]byte(password))
|
||||
|
||||
pubkey := privateKey.PubKey().SerializeCompressed()
|
||||
state := map[string]string{
|
||||
utils.ENCRYPTED_PRVKEY: hex.EncodeToString(encryptedPrivateKey),
|
||||
utils.PASSWORD_HASH: hex.EncodeToString(passwordHash),
|
||||
utils.PUBKEY: hex.EncodeToString(pubkey),
|
||||
}
|
||||
|
||||
if err := utils.SetState(ctx, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("wallet initialized")
|
||||
return nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
)
|
||||
|
||||
func toChainParams(net *common.Network) chaincfg.Params {
|
||||
// we pass nil to have the equivalent of dnssec=0 in bitcoin.conf
|
||||
mutinyNetSigNetParams := chaincfg.CustomSignetParams(common.MutinyNetChallenge, nil)
|
||||
mutinyNetSigNetParams.TargetTimePerBlock = common.MutinyNetBlockTime
|
||||
switch net.Name {
|
||||
case common.Bitcoin.Name:
|
||||
return chaincfg.MainNetParams
|
||||
case common.BitcoinTestNet.Name:
|
||||
return chaincfg.TestNet3Params
|
||||
case common.BitcoinRegTest.Name:
|
||||
return chaincfg.RegressionNetParams
|
||||
case common.BitcoinSigNet.Name:
|
||||
return mutinyNetSigNetParams
|
||||
default:
|
||||
return chaincfg.MainNetParams
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func collaborativeRedeem(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, addr string, amount uint64,
|
||||
) error {
|
||||
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
||||
|
||||
_, err := btcutil.DecodeAddress(addr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid onchain address")
|
||||
}
|
||||
|
||||
netinstate, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dust, err := utils.GetDust(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
netParams := toChainParams(netinstate)
|
||||
|
||||
if netinstate.Name != netParams.Name {
|
||||
return fmt.Errorf("invalid onchain address: must be for %s network", netParams.Name)
|
||||
}
|
||||
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
receivers := []*arkv1.Output{
|
||||
{
|
||||
Address: addr,
|
||||
Amount: amount,
|
||||
},
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, withExpiryCoinselect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedCoins, changeAmount, err := coinSelect(vtxos, amount, withExpiryCoinselect, dust)
|
||||
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{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
secKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ephemeralKey, err := secp256k1.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubkey := hex.EncodeToString(ephemeralKey.PubKey().SerializeCompressed())
|
||||
|
||||
registerResponse, err := client.RegisterPayment(ctx.Context, &arkv1.RegisterPaymentRequest{
|
||||
Inputs: inputs,
|
||||
EphemeralPubkey: &pubkey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
|
||||
Id: registerResponse.GetId(),
|
||||
Outputs: receivers,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx,
|
||||
client,
|
||||
registerResponse.GetId(),
|
||||
selectedCoins,
|
||||
false,
|
||||
secKey,
|
||||
receivers,
|
||||
ephemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"pool_txid": poolTxID,
|
||||
})
|
||||
}
|
||||
|
||||
func unilateralRedeem(ctx *cli.Context, client arkv1.ArkServiceClient) error {
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalVtxosAmount := uint64(0)
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
totalVtxosAmount += vtxo.amount
|
||||
}
|
||||
|
||||
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.Context, 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 {
|
||||
fmt.Printf("error broadcasting tx %s: %s\n", txHex, err)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type redeemBranch struct {
|
||||
vtxo *vtxo
|
||||
branch []*psbt.Packet
|
||||
lifetime time.Duration
|
||||
explorer utils.Explorer
|
||||
}
|
||||
|
||||
func newRedeemBranch(
|
||||
explorer utils.Explorer,
|
||||
congestionTree tree.CongestionTree, vtxo vtxo,
|
||||
) (*redeemBranch, error) {
|
||||
_, seconds, err := findSweepClosure(congestionTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes, err := congestionTree.Branch(vtxo.txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branch := make([]*psbt.Packet, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
branch = append(branch, ptx)
|
||||
}
|
||||
|
||||
return &redeemBranch{
|
||||
vtxo: &vtxo,
|
||||
branch: branch,
|
||||
lifetime: lifetime,
|
||||
explorer: explorer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
|
||||
func (r *redeemBranch) redeemPath() ([]string, error) {
|
||||
transactions := make([]string, 0, len(r.branch))
|
||||
|
||||
offchainPath, err := r.offchainPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ptx := range offchainPath {
|
||||
firstInput := ptx.Inputs[0]
|
||||
if len(firstInput.TaprootKeySpendSig) == 0 {
|
||||
return nil, fmt.Errorf("missing taproot key spend signature")
|
||||
}
|
||||
|
||||
var witness bytes.Buffer
|
||||
|
||||
if err := psbt.WriteTxWitness(&witness, [][]byte{firstInput.TaprootKeySpendSig}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ptx.Inputs[0].FinalScriptWitness = witness.Bytes()
|
||||
|
||||
extracted, err := psbt.Extract(ptx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var txBytes bytes.Buffer
|
||||
|
||||
if err := extracted.Serialize(&txBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transactions = append(transactions, hex.EncodeToString(txBytes.Bytes()))
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func (r *redeemBranch) expireAt(*cli.Context) (*time.Time, error) {
|
||||
lastKnownBlocktime := int64(0)
|
||||
|
||||
confirmed, blocktime, _ := r.explorer.GetTxBlocktime(r.vtxo.poolTxid)
|
||||
|
||||
if confirmed {
|
||||
lastKnownBlocktime = blocktime
|
||||
} else {
|
||||
expirationFromNow := time.Now().Add(time.Minute).Add(r.lifetime)
|
||||
return &expirationFromNow, nil
|
||||
}
|
||||
|
||||
for _, ptx := range r.branch {
|
||||
txid := ptx.UnsignedTx.TxHash().String()
|
||||
|
||||
confirmed, blocktime, err := r.explorer.GetTxBlocktime(txid)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if confirmed {
|
||||
lastKnownBlocktime = blocktime
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
t := time.Unix(lastKnownBlocktime, 0).Add(r.lifetime)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// offchainPath checks for transactions of the branch onchain and returns only the offchain part
|
||||
func (r *redeemBranch) offchainPath() ([]*psbt.Packet, error) {
|
||||
offchainPath := append([]*psbt.Packet{}, r.branch...)
|
||||
|
||||
for i := len(r.branch) - 1; i >= 0; i-- {
|
||||
ptx := r.branch[i]
|
||||
txHash := ptx.UnsignedTx.TxHash().String()
|
||||
|
||||
if _, err := r.explorer.GetTxHex(txHash); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch
|
||||
if i == len(r.branch)-1 {
|
||||
offchainPath = []*psbt.Packet{}
|
||||
} else {
|
||||
offchainPath = r.branch[i+1:]
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return offchainPath, nil
|
||||
}
|
||||
|
||||
func findSweepClosure(
|
||||
congestionTree tree.CongestionTree,
|
||||
) (*txscript.TapLeaf, uint, error) {
|
||||
root, err := congestionTree.Root()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// find the sweep closure
|
||||
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
|
||||
if err != nil {
|
||||
fmt.Println("find sweep closure error")
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var seconds uint
|
||||
var sweepClosure *txscript.TapLeaf
|
||||
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
|
||||
closure := &bitcointree.CSVSigClosure{}
|
||||
valid, err := closure.Decode(tapLeaf.Script)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if valid && closure.Seconds > seconds {
|
||||
seconds = closure.Seconds
|
||||
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
|
||||
sweepClosure = &leaf
|
||||
}
|
||||
}
|
||||
|
||||
if sweepClosure == nil {
|
||||
return nil, 0, fmt.Errorf("sweep closure not found")
|
||||
}
|
||||
|
||||
return sweepClosure, seconds, nil
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
receiverAddr := ctx.String("to")
|
||||
amount := ctx.Uint64("amount")
|
||||
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
||||
|
||||
dust, err := utils.GetDust(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if amount < dust {
|
||||
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
|
||||
}
|
||||
|
||||
if receiverAddr == "" {
|
||||
return fmt.Errorf("receiver address is required")
|
||||
}
|
||||
isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOnchain {
|
||||
txid, err := sendOnchain(ctx, []receiver{{receiverAddr, amount}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
}
|
||||
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, aspPubKey, err := common.DecodeAddress(offchainAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, aspKey, err := common.DecodeAddress(receiverAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid receiver address: %s", err)
|
||||
}
|
||||
if !bytes.Equal(
|
||||
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
|
||||
) {
|
||||
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiverAddr)
|
||||
}
|
||||
|
||||
receiversOutput := make([]*arkv1.Output, 0)
|
||||
sumOfReceivers := uint64(0)
|
||||
receiversOutput = append(receiversOutput, &arkv1.Output{
|
||||
Address: receiverAddr,
|
||||
Amount: amount,
|
||||
})
|
||||
sumOfReceivers += amount
|
||||
|
||||
client, close, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, withExpiryCoinselect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selectedCoins, changeAmount, err := coinSelect(vtxos, sumOfReceivers, withExpiryCoinselect, dust)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if changeAmount > 0 {
|
||||
changeReceiver := &arkv1.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]*arkv1.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := client.CreatePayment(
|
||||
ctx.Context, &arkv1.CreatePaymentRequest{
|
||||
Inputs: inputs,
|
||||
Outputs: receiversOutput,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO verify the redeem tx signature
|
||||
fmt.Println("payment created")
|
||||
fmt.Println("signing redeem and forfeit txs...")
|
||||
|
||||
seckey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedUnconditionalForfeitTxs := make([]string, 0, len(resp.UsignedUnconditionalForfeitTxs))
|
||||
for _, tx := range resp.UsignedUnconditionalForfeitTxs {
|
||||
forfeitPtx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, forfeitPtx, explorer, seckey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedForfeitTx, err := forfeitPtx.B64Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
|
||||
}
|
||||
|
||||
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(resp.SignedRedeemTx), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := signPsbt(ctx, redeemPtx, explorer, seckey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedRedeem, err := redeemPtx.B64Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = client.CompletePayment(ctx.Context, &arkv1.CompletePaymentRequest{
|
||||
SignedRedeemTx: signedRedeem,
|
||||
SignedUnconditionalForfeitTxs: signedUnconditionalForfeitTxs,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("payment completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func coinSelect(vtxos []vtxo, amount uint64, sortByExpirationTime bool, dust uint64) ([]vtxo, uint64, error) {
|
||||
selected := make([]vtxo, 0)
|
||||
notSelected := make([]vtxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
|
||||
if sortByExpirationTime {
|
||||
// sort vtxos by expiration (older first)
|
||||
sort.SliceStable(vtxos, func(i, j int) bool {
|
||||
if vtxos[i].expireAt == nil || vtxos[j].expireAt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return vtxos[i].expireAt.Before(*vtxos[j].expireAt)
|
||||
})
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
if selectedAmount >= amount {
|
||||
notSelected = append(notSelected, vtxo)
|
||||
break
|
||||
}
|
||||
|
||||
selected = append(selected, vtxo)
|
||||
selectedAmount += vtxo.amount
|
||||
}
|
||||
|
||||
if selectedAmount < amount {
|
||||
return nil, 0, fmt.Errorf("not enough funds to cover amount %d", amount)
|
||||
}
|
||||
|
||||
change := selectedAmount - amount
|
||||
|
||||
if change > 0 && change < dust {
|
||||
if len(notSelected) > 0 {
|
||||
selected = append(selected, notSelected[0])
|
||||
change += notSelected[0].amount
|
||||
}
|
||||
}
|
||||
|
||||
return selected, change, nil
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func signPsbt(
|
||||
_ *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
|
||||
) error {
|
||||
updater, err := psbt.NewUpdater(ptx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, input := range updater.Upsbt.UnsignedTx.TxIn {
|
||||
if updater.Upsbt.Inputs[i].WitnessUtxo != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
prevoutTxHex, err := explorer.GetTxHex(input.PreviousOutPoint.Hash.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var prevoutTx wire.MsgTx
|
||||
|
||||
if err := prevoutTx.Deserialize(hex.NewDecoder(strings.NewReader(prevoutTxHex))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utxo := prevoutTx.TxOut[input.PreviousOutPoint.Index]
|
||||
if utxo == nil {
|
||||
return fmt.Errorf("witness utxo not found")
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(utxo, i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
prevouts := make(map[wire.OutPoint]*wire.TxOut)
|
||||
|
||||
for i, input := range updater.Upsbt.Inputs {
|
||||
outpoint := updater.Upsbt.UnsignedTx.TxIn[i].PreviousOutPoint
|
||||
prevouts[outpoint] = input.WitnessUtxo
|
||||
}
|
||||
|
||||
prevoutFetcher := txscript.NewMultiPrevOutFetcher(
|
||||
prevouts,
|
||||
)
|
||||
|
||||
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
|
||||
|
||||
for i, input := range ptx.Inputs {
|
||||
if len(input.TaprootLeafScript) > 0 {
|
||||
pubkey := prvKey.PubKey()
|
||||
for _, leaf := range input.TaprootLeafScript {
|
||||
closure, err := bitcointree.DecodeClosure(leaf.Script)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sign := false
|
||||
|
||||
switch c := closure.(type) {
|
||||
case *bitcointree.CSVSigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
|
||||
case *bitcointree.MultisigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
|
||||
}
|
||||
|
||||
if sign {
|
||||
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := txscript.NewTapLeaf(leaf.LeafVersion, leaf.Script).TapHash()
|
||||
|
||||
preimage, err := txscript.CalcTapscriptSignaturehash(
|
||||
txsighashes,
|
||||
txscript.SigHashDefault,
|
||||
ptx.UnsignedTx,
|
||||
i,
|
||||
prevoutFetcher,
|
||||
txscript.NewBaseTapLeaf(leaf.Script),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig, err := schnorr.Sign(
|
||||
prvKey,
|
||||
preimage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !sig.Verify(preimage, prvKey.PubKey()) {
|
||||
return fmt.Errorf("signature verification failed")
|
||||
}
|
||||
|
||||
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{
|
||||
{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(prvKey.PubKey()),
|
||||
LeafHash: hash.CloneBytes(),
|
||||
Signature: sig.Serialize(),
|
||||
SigHash: txscript.SigHashDefault,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package covenantless
|
||||
|
||||
import (
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
func computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint,
|
||||
) (*secp256k1.PublicKey, *txscript.TapscriptProof, error) {
|
||||
redeemClosure := &bitcointree.CSVSigClosure{
|
||||
Pubkey: userPubkey,
|
||||
Seconds: exitDelay,
|
||||
}
|
||||
|
||||
forfeitClosure := &bitcointree.MultisigClosure{
|
||||
Pubkey: userPubkey,
|
||||
AspPubkey: aspPubkey,
|
||||
}
|
||||
|
||||
redeemLeaf, err := redeemClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
vtxoTaprootTree := txscript.AssembleTaprootScriptTree(
|
||||
*redeemLeaf, *forfeitLeaf,
|
||||
)
|
||||
root := vtxoTaprootTree.RootNode.TapHash()
|
||||
|
||||
unspendableKey := bitcointree.UnspendableKey()
|
||||
vtxoTaprootKey := txscript.ComputeTaprootOutputKey(unspendableKey, root[:])
|
||||
|
||||
redeemLeafHash := redeemLeaf.TapHash()
|
||||
proofIndex := vtxoTaprootTree.LeafProofIndex[redeemLeafHash]
|
||||
proof := vtxoTaprootTree.LeafMerkleProofs[proofIndex]
|
||||
|
||||
return vtxoTaprootKey, &proof, nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const DATADIR_ENVVAR = "ARK_WALLET_DATADIR"
|
||||
|
||||
var (
|
||||
DatadirFlag = &cli.StringFlag{
|
||||
Name: "datadir",
|
||||
Usage: "Specify the data directory",
|
||||
Required: false,
|
||||
Value: common.AppDataDir("ark-cli", false),
|
||||
EnvVars: []string{DATADIR_ENVVAR},
|
||||
}
|
||||
PasswordFlag = cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "password to unlock the wallet",
|
||||
Required: false,
|
||||
Hidden: true,
|
||||
}
|
||||
ExpiryDetailsFlag = cli.BoolFlag{
|
||||
Name: "compute-expiry-details",
|
||||
Usage: "compute client-side the VTXOs expiry time",
|
||||
Value: false,
|
||||
Required: false,
|
||||
}
|
||||
PrivateKeyFlag = cli.StringFlag{
|
||||
Name: "prvkey",
|
||||
Usage: "optional, private key to encrypt",
|
||||
}
|
||||
NetworkFlag = cli.StringFlag{
|
||||
Name: "network",
|
||||
Usage: "network to use (liquid, testnet, regtest, signet)",
|
||||
Value: "liquid",
|
||||
}
|
||||
UrlFlag = cli.StringFlag{
|
||||
Name: "asp-url",
|
||||
Usage: "the url of the ASP to connect to",
|
||||
Required: true,
|
||||
}
|
||||
ExplorerFlag = cli.StringFlag{
|
||||
Name: "explorer",
|
||||
Usage: "the url of the explorer to use",
|
||||
}
|
||||
ReceiversFlag = cli.StringFlag{
|
||||
Name: "receivers",
|
||||
Usage: "receivers of the send transaction, JSON encoded: '[{\"to\": \"<...>\", \"amount\": <...>}, ...]'",
|
||||
}
|
||||
ToFlag = cli.StringFlag{
|
||||
Name: "to",
|
||||
Usage: "address of the recipient",
|
||||
}
|
||||
AmountFlag = cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to send in sats",
|
||||
}
|
||||
EnableExpiryCoinselectFlag = cli.BoolFlag{
|
||||
Name: "enable-expiry-coinselect",
|
||||
Usage: "select vtxos that are about to expire first",
|
||||
Value: false,
|
||||
}
|
||||
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,
|
||||
}
|
||||
AsyncPaymentFlag = cli.BoolFlag{
|
||||
Name: "async",
|
||||
Usage: "use async payment protocol",
|
||||
Value: false,
|
||||
Required: false,
|
||||
}
|
||||
)
|
||||
@@ -5,21 +5,18 @@ go 1.23.1
|
||||
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
|
||||
require (
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899
|
||||
github.com/ark-network/ark/common v0.0.0-20240910195127-ab2c9785d00e
|
||||
github.com/btcsuite/btcd v0.24.2
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/urfave/cli/v2 v2.27.4
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/term v0.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/aead/siphash v1.0.1 // indirect
|
||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect
|
||||
@@ -30,12 +27,15 @@ require (
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/continuity v0.4.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/decred/dcrd/lru v1.1.3 // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kkdai/bstream v1.0.0 // indirect
|
||||
@@ -66,23 +66,18 @@ require (
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/vulpemventures/go-elements v0.5.4
|
||||
github.com/vulpemventures/go-elements v0.5.4 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
||||
@@ -9,8 +9,6 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
|
||||
github.com/ark-network/ark/common v0.0.0-20240910195127-ab2c9785d00e h1:v/8OJWtbOP+663V7n4a6JJz19GQ3+zoKxxPtvqB0a94=
|
||||
github.com/ark-network/ark/common v0.0.0-20240910195127-ab2c9785d00e/go.mod h1:VJQZ+5oRGfMKv4euwgoblBQiI6yBOi9JuNUb3SOpOiU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -124,8 +122,6 @@ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package interfaces
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
|
||||
type CLI interface {
|
||||
Balance(ctx *cli.Context) error
|
||||
Init(ctx *cli.Context) error
|
||||
Receive(ctx *cli.Context) error
|
||||
Redeem(ctx *cli.Context) error
|
||||
Send(ctx *cli.Context) error
|
||||
Claim(ctx *cli.Context) error
|
||||
SendAsync(ctx *cli.Context) error
|
||||
}
|
||||
645
client/main.go
645
client/main.go
@@ -1,174 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ark-network/ark/client/covenant"
|
||||
"github.com/ark-network/ark/client/covenantless"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/interfaces"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
arksdk "github.com/ark-network/ark/pkg/client-sdk"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||
filestore "github.com/ark-network/ark/pkg/client-sdk/store/file"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
DatadirEnvVar = "ARK_WALLET_DATADIR"
|
||||
)
|
||||
|
||||
var (
|
||||
balanceCommand = cli.Command{
|
||||
Name: "balance",
|
||||
Usage: "Shows the onchain and offchain balance of the Ark wallet",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Balance(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.ExpiryDetailsFlag},
|
||||
}
|
||||
|
||||
configCommand = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Shows configuration of the Ark wallet",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
state, err := utils.GetState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(state)
|
||||
},
|
||||
}
|
||||
|
||||
dumpCommand = cli.Command{
|
||||
Name: "dump-privkey",
|
||||
Usage: "Dumps private key of the Ark wallet",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
privKey, err := utils.PrivateKeyFromPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"private_key": hex.EncodeToString(privKey.Serialize()),
|
||||
})
|
||||
},
|
||||
Flags: []cli.Flag{&flags.PasswordFlag},
|
||||
}
|
||||
|
||||
initCommand = cli.Command{
|
||||
Name: "init",
|
||||
Usage: "Initialize your Ark wallet with an encryption password, and connect it to an ASP",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromFlags(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cli.Init(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.PasswordFlag, &flags.PrivateKeyFlag, &flags.NetworkFlag, &flags.UrlFlag, &flags.ExplorerFlag},
|
||||
}
|
||||
|
||||
sendCommand = cli.Command{
|
||||
Name: "send",
|
||||
Usage: "Send your onchain or offchain funds to one or many receivers",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
state, err := utils.GetState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
networkName := state[utils.NETWORK]
|
||||
cli, err := getCLI(networkName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(networkName, "liquid") {
|
||||
return cli.Send(ctx)
|
||||
}
|
||||
return cli.SendAsync(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.ReceiversFlag, &flags.ToFlag, &flags.AmountFlag, &flags.PasswordFlag, &flags.EnableExpiryCoinselectFlag, &flags.AsyncPaymentFlag},
|
||||
}
|
||||
|
||||
claimCommand = cli.Command{
|
||||
Name: "claim",
|
||||
Usage: "Join round to claim pending payments",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Claim(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.PasswordFlag},
|
||||
}
|
||||
|
||||
receiveCommand = cli.Command{
|
||||
Name: "receive",
|
||||
Usage: "Shows both onchain and offchain addresses",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cli.Receive(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
redeemCommand = cli.Command{
|
||||
Name: "redeem",
|
||||
Usage: "Redeem your offchain funds, either collaboratively or unilaterally",
|
||||
Flags: []cli.Flag{&flags.AddressFlag, &flags.AmountToRedeemFlag, &flags.ForceFlag, &flags.PasswordFlag, &flags.EnableExpiryCoinselectFlag},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Redeem(ctx)
|
||||
},
|
||||
}
|
||||
version = "alpha"
|
||||
arkSdkClient arksdk.ArkClient
|
||||
)
|
||||
|
||||
var Version string
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Version = version
|
||||
app.Name = "Ark CLI"
|
||||
app.Version = Version
|
||||
app.Usage = "Ark wallet command line interface"
|
||||
app.Usage = "ark wallet command line interface"
|
||||
app.Commands = append(
|
||||
app.Commands,
|
||||
&balanceCommand,
|
||||
&initCommand,
|
||||
&configCommand,
|
||||
&dumpCommand,
|
||||
&initCommand,
|
||||
&receiveCommand,
|
||||
&redeemCommand,
|
||||
&claimCmd,
|
||||
&sendCommand,
|
||||
&claimCommand,
|
||||
&balanceCommand,
|
||||
&redeemCommand,
|
||||
)
|
||||
app.Flags = []cli.Flag{
|
||||
flags.DatadirFlag,
|
||||
datadirFlag,
|
||||
networkFlag,
|
||||
}
|
||||
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
datadir := cleanAndExpandPath(ctx.String("datadir"))
|
||||
|
||||
if err := ctx.Set("datadir", datadir); err != nil {
|
||||
return err
|
||||
sdk, err := getArkSdkClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing ark sdk client: %v", err)
|
||||
}
|
||||
arkSdkClient = sdk
|
||||
|
||||
if _, err := os.Stat(datadir); os.IsNotExist(err) {
|
||||
return os.Mkdir(datadir, os.ModeDir|0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -179,54 +64,452 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func getCLIFromState(ctx *cli.Context) (interfaces.CLI, error) {
|
||||
state, err := utils.GetState(ctx)
|
||||
var (
|
||||
datadirFlag = &cli.StringFlag{
|
||||
Name: "datadir",
|
||||
Usage: "Specify the data directory",
|
||||
Required: false,
|
||||
Value: common.AppDataDir("ark-cli", false),
|
||||
EnvVars: []string{DatadirEnvVar},
|
||||
}
|
||||
networkFlag = &cli.StringFlag{
|
||||
Name: "network",
|
||||
Usage: "network to use liquid, testnet, regtest, signet for bitcoin, or liquid, liquidtestnet, liquidregtest for liquid)",
|
||||
Value: "liquid",
|
||||
}
|
||||
explorerFlag = &cli.StringFlag{
|
||||
Name: "explorer",
|
||||
Usage: "the url of the explorer to use",
|
||||
}
|
||||
passwordFlag = &cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "password to unlock the wallet",
|
||||
}
|
||||
expiryDetailsFlag = &cli.BoolFlag{
|
||||
Name: "compute-expiry-details",
|
||||
Usage: "compute client-side VTXOs expiry time",
|
||||
}
|
||||
privateKeyFlag = &cli.StringFlag{
|
||||
Name: "prvkey",
|
||||
Usage: "optional private key to encrypt",
|
||||
}
|
||||
urlFlag = &cli.StringFlag{
|
||||
Name: "asp-url",
|
||||
Usage: "the url of the ASP to connect to",
|
||||
Required: true,
|
||||
}
|
||||
receiversFlag = &cli.StringFlag{
|
||||
Name: "receivers",
|
||||
Usage: "JSON encoded receivers of the send transaction",
|
||||
}
|
||||
toFlag = &cli.StringFlag{
|
||||
Name: "to",
|
||||
Usage: "recipient address",
|
||||
}
|
||||
amountFlag = &cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to send in sats",
|
||||
}
|
||||
enableExpiryCoinselectFlag = &cli.BoolFlag{
|
||||
Name: "enable-expiry-coinselect",
|
||||
Usage: "select VTXOs about to expire first",
|
||||
}
|
||||
addressFlag = &cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "main chain address receiving the redeemed VTXO",
|
||||
}
|
||||
amountToRedeemFlag = &cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to redeem",
|
||||
}
|
||||
forceFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "force redemption without collaboration",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
initCommand = cli.Command{
|
||||
Name: "init",
|
||||
Usage: "Initialize Ark wallet with encryption password, connect to ASP",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return initArkSdk(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{networkFlag, passwordFlag, privateKeyFlag, urlFlag, explorerFlag},
|
||||
}
|
||||
configCommand = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Shows Ark wallet configuration",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return config(ctx)
|
||||
},
|
||||
}
|
||||
dumpCommand = cli.Command{
|
||||
Name: "dump-privkey",
|
||||
Usage: "Dumps private key of the Ark wallet",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return dumpPrivKey(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{passwordFlag},
|
||||
}
|
||||
receiveCommand = cli.Command{
|
||||
Name: "receive",
|
||||
Usage: "Shows boarding and offchain addresses",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return receive(ctx)
|
||||
},
|
||||
}
|
||||
claimCmd = cli.Command{
|
||||
Name: "claim",
|
||||
Usage: "Claim onboarding funds or pending payments",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return claim(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{passwordFlag},
|
||||
}
|
||||
balanceCommand = cli.Command{
|
||||
Name: "balance",
|
||||
Usage: "Shows onchain and offchain Ark wallet balance",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return balance(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{expiryDetailsFlag},
|
||||
}
|
||||
sendCommand = cli.Command{
|
||||
Name: "send",
|
||||
Usage: "Send funds onchain, offchain, or asynchronously",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return send(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{receiversFlag, toFlag, amountFlag, enableExpiryCoinselectFlag, passwordFlag},
|
||||
}
|
||||
redeemCommand = cli.Command{
|
||||
Name: "redeem",
|
||||
Usage: "Redeem offchain funds, collaboratively or unilaterally",
|
||||
Flags: []cli.Flag{addressFlag, amountToRedeemFlag, forceFlag, passwordFlag},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
return redeem(ctx)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func initArkSdk(ctx *cli.Context) error {
|
||||
password, err := readPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return arkSdkClient.Init(
|
||||
ctx.Context, arksdk.InitArgs{
|
||||
ClientType: arksdk.GrpcClient,
|
||||
WalletType: arksdk.SingleKeyWallet,
|
||||
AspUrl: ctx.String(urlFlag.Name),
|
||||
Seed: ctx.String(privateKeyFlag.Name),
|
||||
Password: string(password),
|
||||
ExplorerURL: ctx.String(explorerFlag.Name),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func config(ctx *cli.Context) error {
|
||||
cfgStore, err := getConfigStore(ctx.String(datadirFlag.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgData, err := cfgStore.GetData(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := map[string]interface{}{
|
||||
"asp_url": cfgData.AspUrl,
|
||||
"asp_pubkey": hex.EncodeToString(cfgData.AspPubkey.SerializeCompressed()),
|
||||
"wallet_type": cfgData.WalletType,
|
||||
"client_tyep": cfgData.ClientType,
|
||||
"network": cfgData.Network.Name,
|
||||
"round_lifetime": cfgData.RoundLifetime,
|
||||
"unilateral_exit_delay": cfgData.UnilateralExitDelay,
|
||||
"dust": cfgData.Dust,
|
||||
"boarding_descriptor_template": cfgData.BoardingDescriptorTemplate,
|
||||
"explorer_url": cfgData.ExplorerURL,
|
||||
}
|
||||
|
||||
return printJSON(cfg)
|
||||
}
|
||||
|
||||
func dumpPrivKey(ctx *cli.Context) error {
|
||||
password, err := readPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := arkSdkClient.Unlock(ctx.Context, string(password)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateKey, err := arkSdkClient.Dump(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printJSON(map[string]interface{}{
|
||||
"private_key": privateKey,
|
||||
})
|
||||
}
|
||||
|
||||
func receive(ctx *cli.Context) error {
|
||||
offchainAddr, boardingAddr, err := arkSdkClient.Receive(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(map[string]interface{}{
|
||||
"boarding_address": boardingAddr,
|
||||
"offchain_address": offchainAddr,
|
||||
})
|
||||
}
|
||||
|
||||
func claim(ctx *cli.Context) error {
|
||||
password, err := readPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := arkSdkClient.Unlock(ctx.Context, string(password)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txID, err := arkSdkClient.Claim(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(map[string]interface{}{
|
||||
"txid": txID,
|
||||
})
|
||||
}
|
||||
|
||||
func send(ctx *cli.Context) error {
|
||||
receiversJSON := ctx.String(receiversFlag.Name)
|
||||
to := ctx.String(toFlag.Name)
|
||||
amount := ctx.Uint64(amountFlag.Name)
|
||||
if receiversJSON == "" && to == "" && amount == 0 {
|
||||
return fmt.Errorf("missing destination, use --to and --amount or --receivers")
|
||||
}
|
||||
|
||||
configStore, err := getConfigStore(ctx.String(datadirFlag.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfgData, err := configStore.GetData(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
net := getNetwork(ctx, cfgData)
|
||||
isBitcoin := isBtcChain(net)
|
||||
|
||||
var receivers []arksdk.Receiver
|
||||
if receiversJSON != "" {
|
||||
receivers, err = parseReceivers(receiversJSON, isBitcoin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if isBitcoin {
|
||||
receivers = []arksdk.Receiver{arksdk.NewBitcoinReceiver(to, amount)}
|
||||
} else {
|
||||
receivers = []arksdk.Receiver{arksdk.NewLiquidReceiver(to, amount)}
|
||||
}
|
||||
}
|
||||
|
||||
password, err := readPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := arkSdkClient.Unlock(ctx.Context, string(password)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isBitcoin {
|
||||
return sendCovenantLess(ctx, receivers)
|
||||
}
|
||||
return sendCovenant(ctx.Context, receivers)
|
||||
}
|
||||
|
||||
func balance(ctx *cli.Context) error {
|
||||
computeExpiration := ctx.Bool(expiryDetailsFlag.Name)
|
||||
bal, err := arkSdkClient.Balance(ctx.Context, computeExpiration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(bal)
|
||||
}
|
||||
|
||||
func redeem(ctx *cli.Context) error {
|
||||
password, err := readPassword(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := arkSdkClient.Unlock(ctx.Context, string(password)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
force := ctx.Bool(forceFlag.Name)
|
||||
address := ctx.String(addressFlag.Name)
|
||||
amount := ctx.Uint64(amountToRedeemFlag.Name)
|
||||
computeExpiration := ctx.Bool(expiryDetailsFlag.Name)
|
||||
if force {
|
||||
err := arkSdkClient.UnilateralRedeem(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
txID, err := arkSdkClient.CollaborativeRedeem(
|
||||
ctx.Context, address, amount, computeExpiration,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(map[string]interface{}{
|
||||
"txid": txID,
|
||||
})
|
||||
}
|
||||
|
||||
func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) {
|
||||
cfgStore, err := getConfigStore(ctx.String(datadirFlag.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
networkName := state[utils.NETWORK]
|
||||
return getCLI(networkName)
|
||||
}
|
||||
|
||||
func getCLIFromFlags(ctx *cli.Context) (interfaces.CLI, error) {
|
||||
networkName := strings.ToLower(ctx.String("network"))
|
||||
return getCLI(networkName)
|
||||
}
|
||||
|
||||
func getCLI(networkName string) (interfaces.CLI, error) {
|
||||
switch networkName {
|
||||
case common.Liquid.Name, common.LiquidTestNet.Name, common.LiquidRegTest.Name:
|
||||
return covenant.New(), nil
|
||||
case common.Bitcoin.Name, common.BitcoinTestNet.Name, common.BitcoinRegTest.Name, common.BitcoinSigNet.Name:
|
||||
return covenantless.New(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown network (%s)", networkName)
|
||||
cfgData, err := cfgStore.GetData(ctx.Context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
net := getNetwork(ctx, cfgData)
|
||||
|
||||
if isBtcChain(net) {
|
||||
return loadOrCreateClient(
|
||||
arksdk.LoadCovenantlessClient, arksdk.NewCovenantlessClient, cfgStore,
|
||||
)
|
||||
}
|
||||
return loadOrCreateClient(
|
||||
arksdk.LoadCovenantClient, arksdk.NewCovenantClient, cfgStore,
|
||||
)
|
||||
}
|
||||
|
||||
// cleanAndExpandPath expands environment variables and leading ~ in the
|
||||
// passed path, cleans the result, and returns it.
|
||||
// This function is taken from https://github.com/btcsuite/btcd
|
||||
func cleanAndExpandPath(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Expand initial ~ to OS specific home directory.
|
||||
if strings.HasPrefix(path, "~") {
|
||||
var homeDir string
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
homeDir = u.HomeDir
|
||||
} else {
|
||||
homeDir = os.Getenv("HOME")
|
||||
func loadOrCreateClient(
|
||||
loadFunc, newFunc func(store.ConfigStore) (arksdk.ArkClient, error),
|
||||
store store.ConfigStore,
|
||||
) (arksdk.ArkClient, error) {
|
||||
client, err := loadFunc(store)
|
||||
if err != nil {
|
||||
if errors.Is(err, arksdk.ErrNotInitialized) {
|
||||
return newFunc(store)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
path = strings.Replace(path, "~", homeDir, 1)
|
||||
func getConfigStore(dataDir string) (store.ConfigStore, error) {
|
||||
return filestore.NewConfigStore(dataDir)
|
||||
}
|
||||
|
||||
func getNetwork(ctx *cli.Context, configData *store.StoreData) string {
|
||||
if configData == nil {
|
||||
return strings.ToLower(ctx.String(networkFlag.Name))
|
||||
}
|
||||
return configData.Network.Name
|
||||
}
|
||||
|
||||
func isBtcChain(network string) bool {
|
||||
return network == common.Bitcoin.Name ||
|
||||
network == common.BitcoinTestNet.Name ||
|
||||
network == common.BitcoinSigNet.Name ||
|
||||
network == common.BitcoinRegTest.Name
|
||||
}
|
||||
|
||||
func parseReceivers(receveirsJSON string, isBitcoin bool) ([]arksdk.Receiver, error) {
|
||||
list := make([]map[string]interface{}, 0)
|
||||
if err := json.Unmarshal([]byte(receveirsJSON), &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivers := make([]arksdk.Receiver, 0, len(list))
|
||||
if isBitcoin {
|
||||
for _, v := range list {
|
||||
receivers = append(receivers, arksdk.NewBitcoinReceiver(
|
||||
v["to"].(string), uint64(v["amount"].(float64)),
|
||||
))
|
||||
}
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
|
||||
// but the variables can still be expanded via POSIX-style $VARIABLE.
|
||||
return filepath.Clean(os.ExpandEnv(path))
|
||||
for _, v := range list {
|
||||
receivers = append(receivers, arksdk.NewLiquidReceiver(
|
||||
v["to"].(string), uint64(v["amount"].(float64)),
|
||||
))
|
||||
}
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
func sendCovenantLess(ctx *cli.Context, receivers []arksdk.Receiver) error {
|
||||
computeExpiration := ctx.Bool(enableExpiryCoinselectFlag.Name)
|
||||
txID, err := arkSdkClient.SendAsync(
|
||||
ctx.Context, computeExpiration, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(map[string]interface{}{"txid": txID})
|
||||
}
|
||||
|
||||
func sendCovenant(ctx context.Context, receivers []arksdk.Receiver) error {
|
||||
var onchainReceivers, offchainReceivers []arksdk.Receiver
|
||||
|
||||
for _, receiver := range receivers {
|
||||
if receiver.IsOnchain() {
|
||||
onchainReceivers = append(onchainReceivers, receiver)
|
||||
} else {
|
||||
offchainReceivers = append(offchainReceivers, receiver)
|
||||
}
|
||||
}
|
||||
|
||||
if len(onchainReceivers) > 0 {
|
||||
txID, err := arkSdkClient.SendOnChain(ctx, onchainReceivers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(map[string]interface{}{"txid": txID})
|
||||
}
|
||||
|
||||
if len(offchainReceivers) > 0 {
|
||||
txID, err := arkSdkClient.SendOffChain(ctx, false, offchainReceivers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(map[string]interface{}{"txid": txID})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPassword(ctx *cli.Context) ([]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(syscall.Stdin)
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func printJSON(resp interface{}) error {
|
||||
jsonBytes, err := json.MarshalIndent(resp, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
type cypher struct{}
|
||||
|
||||
func NewAES128Cypher() *cypher {
|
||||
return &cypher{}
|
||||
}
|
||||
|
||||
func (c *cypher) Encrypt(privateKey, password []byte) ([]byte, error) {
|
||||
// Due to https://github.com/golang/go/issues/7168.
|
||||
// This call makes sure that memory is freed in case the GC doesn't do that
|
||||
// right after the encryption/decryption.
|
||||
defer debug.FreeOSMemory()
|
||||
|
||||
if len(privateKey) == 0 {
|
||||
return nil, fmt.Errorf("missing plaintext private key")
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return nil, fmt.Errorf("missing encryption password")
|
||||
}
|
||||
|
||||
key, salt, err := deriveKey(password, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(blockCipher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, privateKey, nil)
|
||||
ciphertext = append(ciphertext, salt...)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func (c *cypher) decrypt(encrypted, password []byte) ([]byte, error) {
|
||||
defer debug.FreeOSMemory()
|
||||
|
||||
if len(encrypted) == 0 {
|
||||
return nil, fmt.Errorf("missing encrypted mnemonic")
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return nil, fmt.Errorf("missing decryption password")
|
||||
}
|
||||
|
||||
salt := encrypted[len(encrypted)-32:]
|
||||
data := encrypted[:len(encrypted)-32]
|
||||
|
||||
key, _, err := deriveKey(password, salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(blockCipher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce, text := data[:gcm.NonceSize()], data[gcm.NonceSize():]
|
||||
// #nosec G407
|
||||
plaintext, err := gcm.Open(nil, nonce, text, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid password")
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// deriveKey derives a 32 byte array key from a custom passhprase
|
||||
func deriveKey(password, salt []byte) ([]byte, []byte, error) {
|
||||
if salt == nil {
|
||||
salt = make([]byte, 32)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
// 2^20 = 1048576 recommended length for key-stretching
|
||||
// check the doc for other recommended values:
|
||||
// https://godoc.org/golang.org/x/crypto/scrypt
|
||||
key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return key, salt, nil
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
type ExplorerUtxo struct {
|
||||
Txid string `json:"txid"`
|
||||
Vout uint32 `json:"vout"`
|
||||
Amount uint64 `json:"value"`
|
||||
Asset string `json:"asset,omitempty"` // optional
|
||||
Status struct {
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Blocktime int64 `json:"block_time"`
|
||||
} `json:"status"`
|
||||
}
|
||||
|
||||
type Explorer interface {
|
||||
GetTxHex(txid string) (string, error)
|
||||
Broadcast(txHex string) (string, error)
|
||||
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
||||
GetBalance(addr, asset string) (uint64, error)
|
||||
GetDelayedBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
) (uint64, map[int64]uint64, error)
|
||||
GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error)
|
||||
GetFeeRate() (float64, error)
|
||||
}
|
||||
|
||||
type explorer struct {
|
||||
cache map[string]string
|
||||
baseUrl string
|
||||
}
|
||||
|
||||
func NewExplorer(ctx *cli.Context) Explorer {
|
||||
baseUrl, err := getBaseURL(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &explorer{
|
||||
cache: make(map[string]string),
|
||||
baseUrl: baseUrl,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *explorer) GetFeeRate() (float64, error) {
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "fee-estimates")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]float64
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("error getting fee rate: %s", resp.Status)
|
||||
}
|
||||
|
||||
if len(response) == 0 {
|
||||
fmt.Println("empty fee-estimates response, default to 2 sat/vbyte")
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
return response["1"], nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetTxHex(txid string) (string, error) {
|
||||
if hex, ok := e.cache[txid]; ok {
|
||||
return hex, nil
|
||||
}
|
||||
|
||||
txHex, err := e.getTxHex(txid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
e.cache[txid] = txHex
|
||||
|
||||
return txHex, nil
|
||||
}
|
||||
|
||||
func (e *explorer) Broadcast(txStr string) (string, error) {
|
||||
clone := strings.Clone(txStr)
|
||||
txStr, txid, err := parseLiquidTx(txStr)
|
||||
if err != nil {
|
||||
txStr, txid, err = parseBitcoinTx(clone)
|
||||
if err != nil {
|
||||
fmt.Println("error parsing tx hex")
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
e.cache[txid] = txStr
|
||||
|
||||
txid, err = e.broadcast(txStr)
|
||||
if err != nil {
|
||||
if strings.Contains(
|
||||
strings.ToLower(err.Error()), "transaction already in block chain",
|
||||
) {
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetUtxos(addr string) ([]ExplorerUtxo, error) {
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "address", addr, "utxo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf(string(body))
|
||||
}
|
||||
payload := []ExplorerUtxo{}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
|
||||
payload, err := e.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
balance := uint64(0)
|
||||
for _, p := range payload {
|
||||
if len(asset) > 0 {
|
||||
if p.Asset != asset {
|
||||
continue
|
||||
}
|
||||
}
|
||||
balance += p.Amount
|
||||
}
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetDelayedBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
|
||||
utxos, err := e.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lockedBalance = make(map[int64]uint64, 0)
|
||||
now := time.Now()
|
||||
for _, utxo := range utxos {
|
||||
blocktime := now
|
||||
if utxo.Status.Confirmed {
|
||||
blocktime = time.Unix(utxo.Status.Blocktime, 0)
|
||||
}
|
||||
|
||||
delay := time.Duration(unilateralExitDelay) * time.Second
|
||||
availableAt := blocktime.Add(delay)
|
||||
if availableAt.After(now) {
|
||||
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
|
||||
lockedBalance[availableAt.Unix()] = 0
|
||||
}
|
||||
|
||||
lockedBalance[availableAt.Unix()] += utxo.Amount
|
||||
} else {
|
||||
spendableBalance += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (e *explorer) GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "tx", txid)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, 0, fmt.Errorf(string(body))
|
||||
}
|
||||
|
||||
var tx struct {
|
||||
Status struct {
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Blocktime int64 `json:"block_time"`
|
||||
} `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tx); err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
if !tx.Status.Confirmed {
|
||||
return false, -1, nil
|
||||
}
|
||||
|
||||
return true, tx.Status.Blocktime, nil
|
||||
|
||||
}
|
||||
|
||||
func (e *explorer) getTxHex(txid string) (string, error) {
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "tx", txid, "hex")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.Get(endpoint)
|
||||
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))
|
||||
}
|
||||
|
||||
hex := string(body)
|
||||
e.cache[txid] = hex
|
||||
return hex, nil
|
||||
}
|
||||
|
||||
func (e *explorer) broadcast(txHex string) (string, error) {
|
||||
body := bytes.NewBuffer([]byte(txHex))
|
||||
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "tx")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.Post(endpoint, "text/plain", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyResponse, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf(string(bodyResponse))
|
||||
}
|
||||
|
||||
return string(bodyResponse), nil
|
||||
}
|
||||
|
||||
func parseLiquidTx(txStr string) (string, string, error) {
|
||||
tx, err := transaction.NewTxFromHex(txStr)
|
||||
if err != nil {
|
||||
pset, err := psetv2.NewPsetFromBase64(txStr)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
tx, err = psetv2.Extract(pset)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
txhex, err := tx.ToHex()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
txid := tx.TxHash().String()
|
||||
|
||||
return txhex, txid, nil
|
||||
}
|
||||
|
||||
txhex, err := tx.ToHex()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
txid := tx.TxHash().String()
|
||||
|
||||
return txhex, txid, nil
|
||||
}
|
||||
|
||||
func parseBitcoinTx(txStr string) (string, string, error) {
|
||||
var tx wire.MsgTx
|
||||
|
||||
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txStr))); err != nil {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(txStr), true)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
txFromPartial, err := psbt.Extract(ptx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
tx = *txFromPartial
|
||||
}
|
||||
|
||||
var txBuf bytes.Buffer
|
||||
|
||||
if err := tx.Serialize(&txBuf); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
txhex := hex.EncodeToString(txBuf.Bytes())
|
||||
txid := tx.TxHash().String()
|
||||
|
||||
return txhex, txid, nil
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PrintJSON(resp interface{}) error {
|
||||
jsonBytes, err := json.MarshalIndent(resp, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(jsonBytes))
|
||||
return nil
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
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 verify {
|
||||
if err := verifyPassword(ctx, password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func HashPassword(password []byte) []byte {
|
||||
hash := sha256.Sum256(password)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func verifyPassword(ctx *cli.Context, password []byte) error {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwordHashString := state[PASSWORD_HASH]
|
||||
if len(passwordHashString) <= 0 {
|
||||
return fmt.Errorf("missing password hash")
|
||||
}
|
||||
|
||||
passwordHash, err := hex.DecodeString(passwordHashString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentPassHash := HashPassword(password)
|
||||
|
||||
if !bytes.Equal(passwordHash, currentPassHash) {
|
||||
return fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
ASP_URL = "asp_url"
|
||||
ASP_PUBKEY = "asp_public_key"
|
||||
ROUND_LIFETIME = "round_lifetime"
|
||||
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay"
|
||||
BOARDING_TEMPLATE = "boarding_template"
|
||||
ENCRYPTED_PRVKEY = "encrypted_private_key"
|
||||
PASSWORD_HASH = "password_hash"
|
||||
PUBKEY = "public_key"
|
||||
NETWORK = "network"
|
||||
EXPLORER = "explorer"
|
||||
DUST = "dust"
|
||||
|
||||
defaultNetwork = "liquid"
|
||||
state_file = "state.json"
|
||||
)
|
||||
|
||||
var initialState = map[string]string{
|
||||
ASP_URL: "",
|
||||
ASP_PUBKEY: "",
|
||||
ROUND_LIFETIME: "",
|
||||
UNILATERAL_EXIT_DELAY: "",
|
||||
ENCRYPTED_PRVKEY: "",
|
||||
PASSWORD_HASH: "",
|
||||
PUBKEY: "",
|
||||
DUST: "546",
|
||||
NETWORK: defaultNetwork,
|
||||
}
|
||||
|
||||
func GetNetwork(ctx *cli.Context) (*common.Network, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
net, ok := state[NETWORK]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network not found in state")
|
||||
}
|
||||
return networkFromString(net), nil
|
||||
}
|
||||
|
||||
func GetRoundLifetime(ctx *cli.Context) (int64, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
lifetime := state[ROUND_LIFETIME]
|
||||
if len(lifetime) <= 0 {
|
||||
return -1, fmt.Errorf("missing round lifetime")
|
||||
}
|
||||
|
||||
roundLifetime, err := strconv.Atoi(lifetime)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return int64(roundLifetime), nil
|
||||
}
|
||||
|
||||
func GetUnilateralExitDelay(ctx *cli.Context) (int64, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
delay := state[UNILATERAL_EXIT_DELAY]
|
||||
if len(delay) <= 0 {
|
||||
return -1, fmt.Errorf("missing unilateral exit delay")
|
||||
}
|
||||
|
||||
redeemDelay, err := strconv.Atoi(delay)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return int64(redeemDelay), nil
|
||||
}
|
||||
|
||||
func GetBoardingDescriptor(ctx *cli.Context) (string, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubkey, err := GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
template := state[BOARDING_TEMPLATE]
|
||||
if len(template) <= 0 {
|
||||
return "", fmt.Errorf("missing boarding descriptor template")
|
||||
}
|
||||
|
||||
pubkeyhex := hex.EncodeToString(schnorr.SerializePubKey(pubkey))
|
||||
|
||||
return strings.ReplaceAll(template, "USER", pubkeyhex), nil
|
||||
}
|
||||
|
||||
func GetWalletPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKeyString := state[PUBKEY]
|
||||
if len(publicKeyString) <= 0 {
|
||||
return nil, fmt.Errorf("missing public key")
|
||||
}
|
||||
|
||||
publicKeyBytes, err := hex.DecodeString(publicKeyString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secp256k1.ParsePubKey(publicKeyBytes)
|
||||
}
|
||||
|
||||
func GetAspPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arkPubKey := state[ASP_PUBKEY]
|
||||
if len(arkPubKey) <= 0 {
|
||||
return nil, fmt.Errorf("missing asp public key")
|
||||
}
|
||||
|
||||
pubKeyBytes, err := hex.DecodeString(arkPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secp256k1.ParsePubKey(pubKeyBytes)
|
||||
}
|
||||
|
||||
func GetDust(ctx *cli.Context) (uint64, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
dust := state[DUST]
|
||||
if len(dust) <= 0 {
|
||||
return 0, fmt.Errorf("missing dust")
|
||||
}
|
||||
|
||||
dustAmount, err := strconv.Atoi(dust)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return uint64(dustAmount), nil
|
||||
}
|
||||
|
||||
func GetState(ctx *cli.Context) (map[string]string, error) {
|
||||
datadir := ctx.String("datadir")
|
||||
stateFilePath := filepath.Join(datadir, state_file)
|
||||
file, err := os.ReadFile(stateFilePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if err := setInitialState(stateFilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return initialState, nil
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func PrivateKeyFromPassword(ctx *cli.Context) (*secp256k1.PrivateKey, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedPrivateKeyString := state[ENCRYPTED_PRVKEY]
|
||||
if len(encryptedPrivateKeyString) <= 0 {
|
||||
return nil, fmt.Errorf("missing encrypted private key")
|
||||
}
|
||||
|
||||
encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid encrypted private key: %s", err)
|
||||
}
|
||||
|
||||
password, err := ReadPassword(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("wallet unlocked")
|
||||
|
||||
cypher := NewAES128Cypher()
|
||||
privateKeyBytes, err := cypher.decrypt(encryptedPrivateKey, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func SetState(ctx *cli.Context, data map[string]string) error {
|
||||
currentData, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedData := merge(currentData, data)
|
||||
|
||||
jsonString, err := json.Marshal(mergedData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datadir := ctx.String("datadir")
|
||||
statePath := filepath.Join(datadir, state_file)
|
||||
|
||||
err = os.WriteFile(statePath, jsonString, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func networkFromString(net string) *common.Network {
|
||||
switch net {
|
||||
case common.Liquid.Name:
|
||||
return &common.Liquid
|
||||
case common.LiquidTestNet.Name:
|
||||
return &common.LiquidTestNet
|
||||
case common.LiquidRegTest.Name:
|
||||
return &common.LiquidRegTest
|
||||
case common.Bitcoin.Name:
|
||||
return &common.Bitcoin
|
||||
case common.BitcoinTestNet.Name:
|
||||
return &common.BitcoinTestNet
|
||||
case common.BitcoinRegTest.Name:
|
||||
return &common.BitcoinRegTest
|
||||
case common.BitcoinSigNet.Name:
|
||||
return &common.BitcoinSigNet
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown network (%s)", net))
|
||||
}
|
||||
}
|
||||
|
||||
func setInitialState(stateFilePath string) error {
|
||||
jsonString, err := json.Marshal(initialState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(stateFilePath, jsonString, 0755)
|
||||
}
|
||||
|
||||
func getBaseURL(ctx *cli.Context) (string, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseURL := state[EXPLORER]
|
||||
if len(baseURL) <= 0 {
|
||||
return "", fmt.Errorf("missing explorer base url")
|
||||
}
|
||||
|
||||
return baseURL, nil
|
||||
}
|
||||
|
||||
func merge(maps ...map[string]string) map[string]string {
|
||||
merge := make(map[string]string, 0)
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
merge[k] = v
|
||||
}
|
||||
}
|
||||
return merge
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
)
|
||||
|
||||
type Utxo struct {
|
||||
Txid string
|
||||
Vout uint32
|
||||
Amount uint64
|
||||
Asset string // optional
|
||||
Delay uint
|
||||
SpendableAt time.Time
|
||||
}
|
||||
|
||||
func (u *Utxo) Sequence() (uint32, error) {
|
||||
return common.BIP68EncodeAsNumber(u.Delay)
|
||||
}
|
||||
|
||||
func NewUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
|
||||
utxoTime := explorerUtxo.Status.Blocktime
|
||||
if utxoTime == 0 {
|
||||
utxoTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
return Utxo{
|
||||
Txid: explorerUtxo.Txid,
|
||||
Vout: explorerUtxo.Vout,
|
||||
Amount: explorerUtxo.Amount,
|
||||
Asset: explorerUtxo.Asset,
|
||||
Delay: delay,
|
||||
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,12 @@ type ArkClient interface {
|
||||
Claim(ctx context.Context) (string, error)
|
||||
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error)
|
||||
GetTransactionHistory(ctx context.Context) ([]Transaction, error)
|
||||
Dump(ctx context.Context) (seed string, err error)
|
||||
}
|
||||
|
||||
type Receiver interface {
|
||||
To() string
|
||||
Amount() uint64
|
||||
|
||||
isOnchain() bool
|
||||
IsOnchain() bool
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
@@ -38,6 +39,18 @@ var (
|
||||
ErrNotInitialized = fmt.Errorf("client not initialized")
|
||||
)
|
||||
|
||||
var (
|
||||
defaultNetworks = utils.SupportedType[string]{
|
||||
common.Liquid.Name: "https://blockstream.info/liquid/api",
|
||||
common.LiquidTestNet.Name: "https://blockstream.info/liquidtestnet/api",
|
||||
common.LiquidRegTest.Name: "http://localhost:3001",
|
||||
common.Bitcoin.Name: "https://blockstream.info/api",
|
||||
common.BitcoinTestNet.Name: "https://blockstream.info/testnet/api",
|
||||
common.BitcoinRegTest.Name: "http://localhost:3000",
|
||||
common.BitcoinSigNet.Name: "https://mutinynet.com/api",
|
||||
}
|
||||
)
|
||||
|
||||
type arkClient struct {
|
||||
*store.StoreData
|
||||
wallet wallet.WalletService
|
||||
@@ -74,7 +87,7 @@ func (a *arkClient) InitWithWallet(
|
||||
return fmt.Errorf("failed to connect to asp: %s", err)
|
||||
}
|
||||
|
||||
explorerSvc, err := getExplorer(supportedNetworks, info.Network)
|
||||
explorerSvc, err := getExplorer(args.ExplorerURL, info.Network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup explorer: %s", err)
|
||||
}
|
||||
@@ -138,7 +151,7 @@ func (a *arkClient) Init(
|
||||
return fmt.Errorf("failed to connect to asp: %s", err)
|
||||
}
|
||||
|
||||
explorerSvc, err := getExplorer(supportedNetworks, info.Network)
|
||||
explorerSvc, err := getExplorer(args.ExplorerURL, info.Network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup explorer: %s", err)
|
||||
}
|
||||
@@ -164,6 +177,7 @@ func (a *arkClient) Init(
|
||||
UnilateralExitDelay: info.UnilateralExitDelay,
|
||||
Dust: info.Dust,
|
||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||
ExplorerURL: args.ExplorerURL,
|
||||
}
|
||||
walletSvc, err := getWallet(a.store, &storeData, supportedWallets)
|
||||
if err != nil {
|
||||
@@ -201,6 +215,10 @@ func (a *arkClient) IsLocked(ctx context.Context) bool {
|
||||
return a.wallet.IsLocked()
|
||||
}
|
||||
|
||||
func (a *arkClient) Dump(ctx context.Context) (string, error) {
|
||||
return a.wallet.Dump(ctx)
|
||||
}
|
||||
|
||||
func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
|
||||
offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
@@ -254,15 +272,14 @@ func getClient(
|
||||
return factory(aspUrl)
|
||||
}
|
||||
|
||||
func getExplorer(
|
||||
supportedNetworks utils.SupportedType[string], network string,
|
||||
) (explorer.Explorer, error) {
|
||||
url, ok := supportedNetworks[network]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid network")
|
||||
func getExplorer(explorerURL, network string) (explorer.Explorer, error) {
|
||||
if explorerURL == "" {
|
||||
var ok bool
|
||||
if explorerURL, ok = defaultNetworks[network]; !ok {
|
||||
return nil, fmt.Errorf("invalid network")
|
||||
}
|
||||
}
|
||||
|
||||
return explorer.NewExplorer(url, utils.NetworkFromString(network)), nil
|
||||
return explorer.NewExplorer(explorerURL, utils.NetworkFromString(network)), nil
|
||||
}
|
||||
|
||||
func getWallet(
|
||||
|
||||
@@ -45,7 +45,7 @@ func (r liquidReceiver) Amount() uint64 {
|
||||
return r.amount
|
||||
}
|
||||
|
||||
func (r liquidReceiver) isOnchain() bool {
|
||||
func (r liquidReceiver) IsOnchain() bool {
|
||||
_, err := address.ToOutputScript(r.to)
|
||||
return err == nil
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func LoadCovenantClient(storeSvc store.ConfigStore) (ArkClient, error) {
|
||||
return nil, fmt.Errorf("failed to setup transport client: %s", err)
|
||||
}
|
||||
|
||||
explorerSvc, err := getExplorer(supportedNetworks, data.Network.Name)
|
||||
explorerSvc, err := getExplorer(data.ExplorerURL, data.Network.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func LoadCovenantClientWithWallet(
|
||||
return nil, fmt.Errorf("failed to setup transport client: %s", err)
|
||||
}
|
||||
|
||||
explorerSvc, err := getExplorer(supportedNetworks, data.Network.Name)
|
||||
explorerSvc, err := getExplorer(data.ExplorerURL, data.Network.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
||||
}
|
||||
@@ -287,7 +287,7 @@ func (a *covenantArkClient) SendOnChain(
|
||||
ctx context.Context, receivers []Receiver,
|
||||
) (string, error) {
|
||||
for _, receiver := range receivers {
|
||||
if !receiver.isOnchain() {
|
||||
if !receiver.IsOnchain() {
|
||||
return "", fmt.Errorf("invalid receiver address '%s': must be onchain", receiver.To())
|
||||
}
|
||||
}
|
||||
@@ -300,7 +300,7 @@ func (a *covenantArkClient) SendOffChain(
|
||||
withExpiryCoinselect bool, receivers []Receiver,
|
||||
) (string, error) {
|
||||
for _, receiver := range receivers {
|
||||
if receiver.isOnchain() {
|
||||
if receiver.IsOnchain() {
|
||||
return "", fmt.Errorf("invalid receiver address '%s': must be offchain", receiver.To())
|
||||
}
|
||||
}
|
||||
@@ -1062,7 +1062,7 @@ func (a *covenantArkClient) validateCongestionTree(
|
||||
|
||||
connectors := event.Connectors
|
||||
|
||||
if !utils.IsOnchainOnly(receivers) {
|
||||
if !utils.IsLiquidOnchainOnly(receivers) {
|
||||
if err := tree.ValidateCongestionTree(
|
||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||
); err != nil {
|
||||
@@ -1092,7 +1092,7 @@ func (a *covenantArkClient) validateReceivers(
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := utils.DecodeReceiverAddress(
|
||||
isOnChain, onchainScript, userPubkey, err := utils.ParseLiquidAddress(
|
||||
receiver.Address,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
)
|
||||
|
||||
type bitcoinReceiver struct {
|
||||
@@ -49,7 +48,7 @@ func (r bitcoinReceiver) Amount() uint64 {
|
||||
return r.amount
|
||||
}
|
||||
|
||||
func (r bitcoinReceiver) isOnchain() bool {
|
||||
func (r bitcoinReceiver) IsOnchain() bool {
|
||||
_, err := btcutil.DecodeAddress(r.to, nil)
|
||||
return err == nil
|
||||
}
|
||||
@@ -90,7 +89,7 @@ func LoadCovenantlessClient(storeSvc store.ConfigStore) (ArkClient, error) {
|
||||
return nil, fmt.Errorf("failed to setup transport client: %s", err)
|
||||
}
|
||||
|
||||
explorerSvc, err := getExplorer(supportedNetworks, data.Network.Name)
|
||||
explorerSvc, err := getExplorer(data.ExplorerURL, data.Network.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
||||
}
|
||||
@@ -130,7 +129,7 @@ func LoadCovenantlessClientWithWallet(
|
||||
return nil, fmt.Errorf("failed to setup transport client: %s", err)
|
||||
}
|
||||
|
||||
explorerSvc, err := getExplorer(supportedNetworks, data.Network.Name)
|
||||
explorerSvc, err := getExplorer(data.ExplorerURL, data.Network.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
||||
}
|
||||
@@ -291,7 +290,7 @@ func (a *covenantlessArkClient) SendOnChain(
|
||||
ctx context.Context, receivers []Receiver,
|
||||
) (string, error) {
|
||||
for _, receiver := range receivers {
|
||||
if !receiver.isOnchain() {
|
||||
if !receiver.IsOnchain() {
|
||||
return "", fmt.Errorf("invalid receiver address '%s': must be onchain", receiver.To())
|
||||
}
|
||||
}
|
||||
@@ -304,7 +303,7 @@ func (a *covenantlessArkClient) SendOffChain(
|
||||
withExpiryCoinselect bool, receivers []Receiver,
|
||||
) (string, error) {
|
||||
for _, receiver := range receivers {
|
||||
if receiver.isOnchain() {
|
||||
if receiver.IsOnchain() {
|
||||
return "", fmt.Errorf("invalid receiver address '%s': must be offchain", receiver.To())
|
||||
}
|
||||
}
|
||||
@@ -388,24 +387,11 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
return "", fmt.Errorf("wallet is locked")
|
||||
}
|
||||
|
||||
if _, err := address.ToOutputScript(addr); err != nil {
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
if _, err := btcutil.DecodeAddress(addr, &netParams); err != nil {
|
||||
return "", fmt.Errorf("invalid onchain address")
|
||||
}
|
||||
|
||||
addrNet, err := address.NetworkForAddress(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid onchain address: unknown network")
|
||||
}
|
||||
net := utils.ToElementsNetwork(a.Network)
|
||||
if net.Name != addrNet.Name {
|
||||
return "", fmt.Errorf("invalid onchain address: must be for %s network", net.Name)
|
||||
}
|
||||
|
||||
if isConf, _ := address.IsConfidential(addr); isConf {
|
||||
info, _ := address.FromConfidential(addr)
|
||||
addr = info.Address
|
||||
}
|
||||
|
||||
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -490,8 +476,9 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
return "", fmt.Errorf("missing receivers")
|
||||
}
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
for _, receiver := range receivers {
|
||||
isOnchain, _, _, err := utils.DecodeReceiverAddress(receiver.To())
|
||||
isOnchain, _, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -1234,7 +1221,8 @@ func (a *covenantlessArkClient) validateCongestionTree(
|
||||
return err
|
||||
}
|
||||
|
||||
if !utils.IsOnchainOnly(receivers) {
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
if !utils.IsBitcoinOnchainOnly(receivers, netParams) {
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||
); err != nil {
|
||||
@@ -1263,9 +1251,10 @@ func (a *covenantlessArkClient) validateReceivers(
|
||||
congestionTree tree.CongestionTree,
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := utils.DecodeReceiverAddress(
|
||||
receiver.Address,
|
||||
isOnChain, onchainScript, userPubkey, err := utils.ParseBitcoinAddress(
|
||||
receiver.Address, netParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
@@ -63,7 +65,7 @@ func CoinSelect(
|
||||
return selected, change, nil
|
||||
}
|
||||
|
||||
func DecodeReceiverAddress(addr string) (
|
||||
func ParseLiquidAddress(addr string) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
) {
|
||||
outputScript, err := address.ToOutputScript(addr)
|
||||
@@ -78,9 +80,43 @@ func DecodeReceiverAddress(addr string) (
|
||||
return true, outputScript, nil, nil
|
||||
}
|
||||
|
||||
func IsOnchainOnly(receivers []client.Output) bool {
|
||||
func ParseBitcoinAddress(addr string, net chaincfg.Params) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
) {
|
||||
btcAddr, err := btcutil.DecodeAddress(addr, &net)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
}
|
||||
|
||||
onchainScript, err := txscript.PayToAddrScript(btcAddr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return true, onchainScript, nil, nil
|
||||
}
|
||||
|
||||
func IsBitcoinOnchainOnly(receivers []client.Output, net chaincfg.Params) bool {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, _, _, err := DecodeReceiverAddress(receiver.Address)
|
||||
isOnChain, _, _, err := ParseBitcoinAddress(receiver.Address, net)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isOnChain {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func IsLiquidOnchainOnly(receivers []client.Output) bool {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, _, _, err := ParseLiquidAddress(receiver.Address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type storeData struct {
|
||||
UnilateralExitDelay string `json:"unilateral_exit_delay"`
|
||||
Dust string `json:"dust"`
|
||||
BoardingDescriptorTemplate string `json:"boarding_descriptor_template"`
|
||||
ExplorerURL string `json:"explorer_url"`
|
||||
}
|
||||
|
||||
func (d storeData) isEmpty() bool {
|
||||
@@ -43,6 +44,7 @@ func (d storeData) decode() store.StoreData {
|
||||
dust, _ := strconv.Atoi(d.Dust)
|
||||
buf, _ := hex.DecodeString(d.AspPubkey)
|
||||
aspPubkey, _ := secp256k1.ParsePubKey(buf)
|
||||
explorerURL := d.ExplorerURL
|
||||
return store.StoreData{
|
||||
AspUrl: d.AspUrl,
|
||||
AspPubkey: aspPubkey,
|
||||
@@ -53,6 +55,7 @@ func (d storeData) decode() store.StoreData {
|
||||
UnilateralExitDelay: int64(unilateralExitDelay),
|
||||
Dust: uint64(dust),
|
||||
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
|
||||
ExplorerURL: explorerURL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +70,7 @@ func (d storeData) asMap() map[string]string {
|
||||
"unilateral_exit_delay": d.UnilateralExitDelay,
|
||||
"dust": d.Dust,
|
||||
"boarding_descriptor_template": d.BoardingDescriptorTemplate,
|
||||
"explorer_url": d.ExplorerURL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +116,7 @@ func (s *Store) AddData(ctx context.Context, data store.StoreData) error {
|
||||
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
|
||||
Dust: fmt.Sprintf("%d", data.Dust),
|
||||
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
|
||||
ExplorerURL: data.ExplorerURL,
|
||||
}
|
||||
|
||||
if err := s.write(sd); err != nil {
|
||||
|
||||
@@ -22,6 +22,7 @@ type StoreData struct {
|
||||
UnilateralExitDelay int64
|
||||
Dust uint64
|
||||
BoardingDescriptorTemplate string
|
||||
ExplorerURL string
|
||||
}
|
||||
|
||||
type ConfigStore interface {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
||||
restclient "github.com/ark-network/ark/pkg/client-sdk/client/rest"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
@@ -19,23 +18,15 @@ var (
|
||||
GrpcClient: grpcclient.NewClient,
|
||||
RestClient: restclient.NewClient,
|
||||
}
|
||||
supportedNetworks = utils.SupportedType[string]{
|
||||
common.Liquid.Name: "https://blockstream.info/liquid/api",
|
||||
common.LiquidTestNet.Name: "https://blockstream.info/liquidtestnet/api",
|
||||
common.LiquidRegTest.Name: "http://localhost:3001",
|
||||
common.Bitcoin.Name: "https://blockstream.info/api",
|
||||
common.BitcoinTestNet.Name: "https://blockstream.info/testnet/api",
|
||||
common.BitcoinRegTest.Name: "http://localhost:3000",
|
||||
common.BitcoinSigNet.Name: "https://mutinynet.com/api",
|
||||
}
|
||||
)
|
||||
|
||||
type InitArgs struct {
|
||||
ClientType string
|
||||
WalletType string
|
||||
AspUrl string
|
||||
Seed string
|
||||
Password string
|
||||
ClientType string
|
||||
WalletType string
|
||||
AspUrl string
|
||||
Seed string
|
||||
Password string
|
||||
ExplorerURL string
|
||||
}
|
||||
|
||||
func (a InitArgs) validate() error {
|
||||
@@ -69,11 +60,12 @@ func (a InitArgs) validate() error {
|
||||
}
|
||||
|
||||
type InitWithWalletArgs struct {
|
||||
ClientType string
|
||||
Wallet wallet.WalletService
|
||||
AspUrl string
|
||||
Seed string
|
||||
Password string
|
||||
ClientType string
|
||||
Wallet wallet.WalletService
|
||||
AspUrl string
|
||||
Seed string
|
||||
Password string
|
||||
ExplorerURL string
|
||||
}
|
||||
|
||||
func (a InitWithWalletArgs) validate() error {
|
||||
|
||||
@@ -116,3 +116,16 @@ func (w *singlekeyWallet) Unlock(
|
||||
func (w *singlekeyWallet) IsLocked() bool {
|
||||
return w.privateKey == nil
|
||||
}
|
||||
|
||||
func (w *singlekeyWallet) Dump(ctx context.Context) (string, error) {
|
||||
if w.walletData == nil {
|
||||
return "", fmt.Errorf("wallet not initialized")
|
||||
}
|
||||
|
||||
if w.IsLocked() {
|
||||
return "", fmt.Errorf("wallet is locked")
|
||||
}
|
||||
|
||||
return hex.EncodeToString(w.privateKey.Serialize()), nil
|
||||
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ type WalletService interface {
|
||||
SignTransaction(
|
||||
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
||||
) (signedTx string, err error)
|
||||
Dump(ctx context.Context) (seed string, err error)
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ func TestUnilateralExit(t *testing.T) {
|
||||
err = utils.GenerateBlock()
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
balanceStr, err = runClarkCommand("balance")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
|
||||
|
||||
Reference in New Issue
Block a user