mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 20:54:20 +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:
4
.github/workflows/ark.integration.yaml
vendored
4
.github/workflows/ark.integration.yaml
vendored
@@ -2,14 +2,10 @@ name: ci_integration
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
|
||||||
- "server/**"
|
|
||||||
branches: [master]
|
branches: [master]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
|
||||||
- "server/**"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
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
|
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||||
|
|
||||||
require (
|
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/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
|
github.com/urfave/cli/v2 v2.27.4
|
||||||
golang.org/x/crypto v0.26.0
|
|
||||||
golang.org/x/term v0.23.0
|
golang.org/x/term v0.23.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/aead/siphash v1.0.1 // 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/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd // indirect
|
github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd // indirect
|
||||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // 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/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // 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/containerd/continuity v0.4.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/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/decred/dcrd/lru v1.1.3 // indirect
|
||||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // 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/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kkdai/bstream v1.0.0 // 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 v1.30.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // 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/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/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/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
|
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/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
|
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/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 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
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 h1:v/8OJWtbOP+663V7n4a6JJz19GQ3+zoKxxPtvqB0a94=
|
||||||
github.com/ark-network/ark/common v0.0.0-20240910195127-ab2c9785d00e/go.mod h1:VJQZ+5oRGfMKv4euwgoblBQiI6yBOi9JuNUb3SOpOiU=
|
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=
|
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.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.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.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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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
|
|
||||||
}
|
|
||||||
631
client/main.go
631
client/main.go
@@ -1,174 +1,59 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"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"
|
"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"
|
"github.com/urfave/cli/v2"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DatadirEnvVar = "ARK_WALLET_DATADIR"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
balanceCommand = cli.Command{
|
version = "alpha"
|
||||||
Name: "balance",
|
arkSdkClient arksdk.ArkClient
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version string
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
|
app.Version = version
|
||||||
app.Name = "Ark CLI"
|
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 = append(
|
||||||
app.Commands,
|
app.Commands,
|
||||||
&balanceCommand,
|
&initCommand,
|
||||||
&configCommand,
|
&configCommand,
|
||||||
&dumpCommand,
|
&dumpCommand,
|
||||||
&initCommand,
|
|
||||||
&receiveCommand,
|
&receiveCommand,
|
||||||
&redeemCommand,
|
&claimCmd,
|
||||||
&sendCommand,
|
&sendCommand,
|
||||||
&claimCommand,
|
&balanceCommand,
|
||||||
|
&redeemCommand,
|
||||||
)
|
)
|
||||||
app.Flags = []cli.Flag{
|
app.Flags = []cli.Flag{
|
||||||
flags.DatadirFlag,
|
datadirFlag,
|
||||||
|
networkFlag,
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Before = func(ctx *cli.Context) error {
|
app.Before = func(ctx *cli.Context) error {
|
||||||
datadir := cleanAndExpandPath(ctx.String("datadir"))
|
sdk, err := getArkSdkClient(ctx)
|
||||||
|
if err != nil {
|
||||||
if err := ctx.Set("datadir", datadir); err != nil {
|
return fmt.Errorf("error initializing ark sdk client: %v", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
arkSdkClient = sdk
|
||||||
|
|
||||||
if _, err := os.Stat(datadir); os.IsNotExist(err) {
|
|
||||||
return os.Mkdir(datadir, os.ModeDir|0755)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,54 +64,452 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCLIFromState(ctx *cli.Context) (interfaces.CLI, error) {
|
var (
|
||||||
state, err := utils.GetState(ctx)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cfgData, err := cfgStore.GetData(ctx.Context)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
net := getNetwork(ctx, cfgData)
|
||||||
|
|
||||||
networkName := state[utils.NETWORK]
|
if isBtcChain(net) {
|
||||||
return getCLI(networkName)
|
return loadOrCreateClient(
|
||||||
|
arksdk.LoadCovenantlessClient, arksdk.NewCovenantlessClient, cfgStore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return loadOrCreateClient(
|
||||||
|
arksdk.LoadCovenantClient, arksdk.NewCovenantClient, cfgStore,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCLIFromFlags(ctx *cli.Context) (interfaces.CLI, error) {
|
func loadOrCreateClient(
|
||||||
networkName := strings.ToLower(ctx.String("network"))
|
loadFunc, newFunc func(store.ConfigStore) (arksdk.ArkClient, error),
|
||||||
return getCLI(networkName)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCLI(networkName string) (interfaces.CLI, error) {
|
func getConfigStore(dataDir string) (store.ConfigStore, error) {
|
||||||
switch networkName {
|
return filestore.NewConfigStore(dataDir)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanAndExpandPath expands environment variables and leading ~ in the
|
func getNetwork(ctx *cli.Context, configData *store.StoreData) string {
|
||||||
// passed path, cleans the result, and returns it.
|
if configData == nil {
|
||||||
// This function is taken from https://github.com/btcsuite/btcd
|
return strings.ToLower(ctx.String(networkFlag.Name))
|
||||||
func cleanAndExpandPath(path string) string {
|
}
|
||||||
if path == "" {
|
return configData.Network.Name
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand initial ~ to OS specific home directory.
|
func isBtcChain(network string) bool {
|
||||||
if strings.HasPrefix(path, "~") {
|
return network == common.Bitcoin.Name ||
|
||||||
var homeDir string
|
network == common.BitcoinTestNet.Name ||
|
||||||
u, err := user.Current()
|
network == common.BitcoinSigNet.Name ||
|
||||||
if err == nil {
|
network == common.BitcoinRegTest.Name
|
||||||
homeDir = u.HomeDir
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
homeDir = os.Getenv("HOME")
|
offchainReceivers = append(offchainReceivers, receiver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path = strings.Replace(path, "~", homeDir, 1)
|
if len(onchainReceivers) > 0 {
|
||||||
|
txID, err := arkSdkClient.SendOnChain(ctx, onchainReceivers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printJSON(map[string]interface{}{"txid": txID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
|
if len(offchainReceivers) > 0 {
|
||||||
// but the variables can still be expanded via POSIX-style $VARIABLE.
|
txID, err := arkSdkClient.SendOffChain(ctx, false, offchainReceivers)
|
||||||
return filepath.Clean(os.ExpandEnv(path))
|
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)
|
Claim(ctx context.Context) (string, error)
|
||||||
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error)
|
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error)
|
||||||
GetTransactionHistory(ctx context.Context) ([]Transaction, error)
|
GetTransactionHistory(ctx context.Context) ([]Transaction, error)
|
||||||
|
Dump(ctx context.Context) (seed string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Receiver interface {
|
type Receiver interface {
|
||||||
To() string
|
To() string
|
||||||
Amount() uint64
|
Amount() uint64
|
||||||
|
|
||||||
isOnchain() bool
|
IsOnchain() bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"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/explorer"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||||
@@ -38,6 +39,18 @@ var (
|
|||||||
ErrNotInitialized = fmt.Errorf("client not initialized")
|
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 {
|
type arkClient struct {
|
||||||
*store.StoreData
|
*store.StoreData
|
||||||
wallet wallet.WalletService
|
wallet wallet.WalletService
|
||||||
@@ -74,7 +87,7 @@ func (a *arkClient) InitWithWallet(
|
|||||||
return fmt.Errorf("failed to connect to asp: %s", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to setup explorer: %s", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to setup explorer: %s", err)
|
return fmt.Errorf("failed to setup explorer: %s", err)
|
||||||
}
|
}
|
||||||
@@ -164,6 +177,7 @@ func (a *arkClient) Init(
|
|||||||
UnilateralExitDelay: info.UnilateralExitDelay,
|
UnilateralExitDelay: info.UnilateralExitDelay,
|
||||||
Dust: info.Dust,
|
Dust: info.Dust,
|
||||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||||
|
ExplorerURL: args.ExplorerURL,
|
||||||
}
|
}
|
||||||
walletSvc, err := getWallet(a.store, &storeData, supportedWallets)
|
walletSvc, err := getWallet(a.store, &storeData, supportedWallets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,6 +215,10 @@ func (a *arkClient) IsLocked(ctx context.Context) bool {
|
|||||||
return a.wallet.IsLocked()
|
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) {
|
func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
|
||||||
offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false)
|
offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -254,15 +272,14 @@ func getClient(
|
|||||||
return factory(aspUrl)
|
return factory(aspUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExplorer(
|
func getExplorer(explorerURL, network string) (explorer.Explorer, error) {
|
||||||
supportedNetworks utils.SupportedType[string], network string,
|
if explorerURL == "" {
|
||||||
) (explorer.Explorer, error) {
|
var ok bool
|
||||||
url, ok := supportedNetworks[network]
|
if explorerURL, ok = defaultNetworks[network]; !ok {
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid network")
|
return nil, fmt.Errorf("invalid network")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return explorer.NewExplorer(url, utils.NetworkFromString(network)), nil
|
return explorer.NewExplorer(explorerURL, utils.NetworkFromString(network)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWallet(
|
func getWallet(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (r liquidReceiver) Amount() uint64 {
|
|||||||
return r.amount
|
return r.amount
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r liquidReceiver) isOnchain() bool {
|
func (r liquidReceiver) IsOnchain() bool {
|
||||||
_, err := address.ToOutputScript(r.to)
|
_, err := address.ToOutputScript(r.to)
|
||||||
return err == nil
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ func (a *covenantArkClient) SendOnChain(
|
|||||||
ctx context.Context, receivers []Receiver,
|
ctx context.Context, receivers []Receiver,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
if !receiver.isOnchain() {
|
if !receiver.IsOnchain() {
|
||||||
return "", fmt.Errorf("invalid receiver address '%s': must be onchain", receiver.To())
|
return "", fmt.Errorf("invalid receiver address '%s': must be onchain", receiver.To())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +300,7 @@ func (a *covenantArkClient) SendOffChain(
|
|||||||
withExpiryCoinselect bool, receivers []Receiver,
|
withExpiryCoinselect bool, receivers []Receiver,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
if receiver.isOnchain() {
|
if receiver.IsOnchain() {
|
||||||
return "", fmt.Errorf("invalid receiver address '%s': must be offchain", receiver.To())
|
return "", fmt.Errorf("invalid receiver address '%s': must be offchain", receiver.To())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1062,7 +1062,7 @@ func (a *covenantArkClient) validateCongestionTree(
|
|||||||
|
|
||||||
connectors := event.Connectors
|
connectors := event.Connectors
|
||||||
|
|
||||||
if !utils.IsOnchainOnly(receivers) {
|
if !utils.IsLiquidOnchainOnly(receivers) {
|
||||||
if err := tree.ValidateCongestionTree(
|
if err := tree.ValidateCongestionTree(
|
||||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -1092,7 +1092,7 @@ func (a *covenantArkClient) validateReceivers(
|
|||||||
aspPubkey *secp256k1.PublicKey,
|
aspPubkey *secp256k1.PublicKey,
|
||||||
) error {
|
) error {
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
isOnChain, onchainScript, userPubkey, err := utils.DecodeReceiverAddress(
|
isOnChain, onchainScript, userPubkey, err := utils.ParseLiquidAddress(
|
||||||
receiver.Address,
|
receiver.Address,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/vulpemventures/go-elements/address"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type bitcoinReceiver struct {
|
type bitcoinReceiver struct {
|
||||||
@@ -49,7 +48,7 @@ func (r bitcoinReceiver) Amount() uint64 {
|
|||||||
return r.amount
|
return r.amount
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r bitcoinReceiver) isOnchain() bool {
|
func (r bitcoinReceiver) IsOnchain() bool {
|
||||||
_, err := btcutil.DecodeAddress(r.to, nil)
|
_, err := btcutil.DecodeAddress(r.to, nil)
|
||||||
return err == 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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
return nil, fmt.Errorf("failed to setup explorer: %s", err)
|
||||||
}
|
}
|
||||||
@@ -291,7 +290,7 @@ func (a *covenantlessArkClient) SendOnChain(
|
|||||||
ctx context.Context, receivers []Receiver,
|
ctx context.Context, receivers []Receiver,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
if !receiver.isOnchain() {
|
if !receiver.IsOnchain() {
|
||||||
return "", fmt.Errorf("invalid receiver address '%s': must be onchain", receiver.To())
|
return "", fmt.Errorf("invalid receiver address '%s': must be onchain", receiver.To())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +303,7 @@ func (a *covenantlessArkClient) SendOffChain(
|
|||||||
withExpiryCoinselect bool, receivers []Receiver,
|
withExpiryCoinselect bool, receivers []Receiver,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
if receiver.isOnchain() {
|
if receiver.IsOnchain() {
|
||||||
return "", fmt.Errorf("invalid receiver address '%s': must be offchain", receiver.To())
|
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")
|
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")
|
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)
|
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -490,8 +476,9 @@ func (a *covenantlessArkClient) SendAsync(
|
|||||||
return "", fmt.Errorf("missing receivers")
|
return "", fmt.Errorf("missing receivers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
isOnchain, _, _, err := utils.DecodeReceiverAddress(receiver.To())
|
isOnchain, _, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1234,7 +1221,8 @@ func (a *covenantlessArkClient) validateCongestionTree(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utils.IsOnchainOnly(receivers) {
|
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||||
|
if !utils.IsBitcoinOnchainOnly(receivers, netParams) {
|
||||||
if err := bitcointree.ValidateCongestionTree(
|
if err := bitcointree.ValidateCongestionTree(
|
||||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -1263,9 +1251,10 @@ func (a *covenantlessArkClient) validateReceivers(
|
|||||||
congestionTree tree.CongestionTree,
|
congestionTree tree.CongestionTree,
|
||||||
aspPubkey *secp256k1.PublicKey,
|
aspPubkey *secp256k1.PublicKey,
|
||||||
) error {
|
) error {
|
||||||
|
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
isOnChain, onchainScript, userPubkey, err := utils.DecodeReceiverAddress(
|
isOnChain, onchainScript, userPubkey, err := utils.ParseBitcoinAddress(
|
||||||
receiver.Address,
|
receiver.Address, netParams,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import (
|
|||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/vulpemventures/go-elements/address"
|
"github.com/vulpemventures/go-elements/address"
|
||||||
"github.com/vulpemventures/go-elements/network"
|
"github.com/vulpemventures/go-elements/network"
|
||||||
@@ -63,7 +65,7 @@ func CoinSelect(
|
|||||||
return selected, change, nil
|
return selected, change, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeReceiverAddress(addr string) (
|
func ParseLiquidAddress(addr string) (
|
||||||
bool, []byte, *secp256k1.PublicKey, error,
|
bool, []byte, *secp256k1.PublicKey, error,
|
||||||
) {
|
) {
|
||||||
outputScript, err := address.ToOutputScript(addr)
|
outputScript, err := address.ToOutputScript(addr)
|
||||||
@@ -78,9 +80,43 @@ func DecodeReceiverAddress(addr string) (
|
|||||||
return true, outputScript, nil, nil
|
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 {
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type storeData struct {
|
|||||||
UnilateralExitDelay string `json:"unilateral_exit_delay"`
|
UnilateralExitDelay string `json:"unilateral_exit_delay"`
|
||||||
Dust string `json:"dust"`
|
Dust string `json:"dust"`
|
||||||
BoardingDescriptorTemplate string `json:"boarding_descriptor_template"`
|
BoardingDescriptorTemplate string `json:"boarding_descriptor_template"`
|
||||||
|
ExplorerURL string `json:"explorer_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d storeData) isEmpty() bool {
|
func (d storeData) isEmpty() bool {
|
||||||
@@ -43,6 +44,7 @@ func (d storeData) decode() store.StoreData {
|
|||||||
dust, _ := strconv.Atoi(d.Dust)
|
dust, _ := strconv.Atoi(d.Dust)
|
||||||
buf, _ := hex.DecodeString(d.AspPubkey)
|
buf, _ := hex.DecodeString(d.AspPubkey)
|
||||||
aspPubkey, _ := secp256k1.ParsePubKey(buf)
|
aspPubkey, _ := secp256k1.ParsePubKey(buf)
|
||||||
|
explorerURL := d.ExplorerURL
|
||||||
return store.StoreData{
|
return store.StoreData{
|
||||||
AspUrl: d.AspUrl,
|
AspUrl: d.AspUrl,
|
||||||
AspPubkey: aspPubkey,
|
AspPubkey: aspPubkey,
|
||||||
@@ -53,6 +55,7 @@ func (d storeData) decode() store.StoreData {
|
|||||||
UnilateralExitDelay: int64(unilateralExitDelay),
|
UnilateralExitDelay: int64(unilateralExitDelay),
|
||||||
Dust: uint64(dust),
|
Dust: uint64(dust),
|
||||||
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
|
||||||
|
ExplorerURL: explorerURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ func (d storeData) asMap() map[string]string {
|
|||||||
"unilateral_exit_delay": d.UnilateralExitDelay,
|
"unilateral_exit_delay": d.UnilateralExitDelay,
|
||||||
"dust": d.Dust,
|
"dust": d.Dust,
|
||||||
"boarding_descriptor_template": d.BoardingDescriptorTemplate,
|
"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),
|
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
|
||||||
Dust: fmt.Sprintf("%d", data.Dust),
|
Dust: fmt.Sprintf("%d", data.Dust),
|
||||||
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
|
||||||
|
ExplorerURL: data.ExplorerURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.write(sd); err != nil {
|
if err := s.write(sd); err != nil {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type StoreData struct {
|
|||||||
UnilateralExitDelay int64
|
UnilateralExitDelay int64
|
||||||
Dust uint64
|
Dust uint64
|
||||||
BoardingDescriptorTemplate string
|
BoardingDescriptorTemplate string
|
||||||
|
ExplorerURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigStore interface {
|
type ConfigStore interface {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ark-network/ark/common"
|
|
||||||
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
||||||
restclient "github.com/ark-network/ark/pkg/client-sdk/client/rest"
|
restclient "github.com/ark-network/ark/pkg/client-sdk/client/rest"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||||
@@ -19,15 +18,6 @@ var (
|
|||||||
GrpcClient: grpcclient.NewClient,
|
GrpcClient: grpcclient.NewClient,
|
||||||
RestClient: restclient.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 {
|
type InitArgs struct {
|
||||||
@@ -36,6 +26,7 @@ type InitArgs struct {
|
|||||||
AspUrl string
|
AspUrl string
|
||||||
Seed string
|
Seed string
|
||||||
Password string
|
Password string
|
||||||
|
ExplorerURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a InitArgs) validate() error {
|
func (a InitArgs) validate() error {
|
||||||
@@ -74,6 +65,7 @@ type InitWithWalletArgs struct {
|
|||||||
AspUrl string
|
AspUrl string
|
||||||
Seed string
|
Seed string
|
||||||
Password string
|
Password string
|
||||||
|
ExplorerURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a InitWithWalletArgs) validate() error {
|
func (a InitWithWalletArgs) validate() error {
|
||||||
|
|||||||
@@ -116,3 +116,16 @@ func (w *singlekeyWallet) Unlock(
|
|||||||
func (w *singlekeyWallet) IsLocked() bool {
|
func (w *singlekeyWallet) IsLocked() bool {
|
||||||
return w.privateKey == nil
|
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(
|
SignTransaction(
|
||||||
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
||||||
) (signedTx string, err error)
|
) (signedTx string, err error)
|
||||||
|
Dump(ctx context.Context) (seed string, err error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ func TestUnilateralExit(t *testing.T) {
|
|||||||
err = utils.GenerateBlock()
|
err = utils.GenerateBlock()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
balanceStr, err = runClarkCommand("balance")
|
balanceStr, err = runClarkCommand("balance")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
|
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
|
||||||
|
|||||||
Reference in New Issue
Block a user