Rename folders (#97)

* Rename arkd folder & drop cli

* Rename ark cli folder & update docs

* Update readme

* Fix

* scripts: add build-all

* Add target to build cli for all platforms

* Update build scripts

---------

Co-authored-by: tiero <3596602+tiero@users.noreply.github.com>
This commit is contained in:
Pietralberto Mazza
2024-02-09 19:32:58 +01:00
committed by GitHub
parent 0d8c7bffb2
commit dc00d60585
119 changed files with 154 additions and 449 deletions

1
client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build/

30
client/Makefile Normal file
View File

@@ -0,0 +1,30 @@
.PHONY: build build-all clean help lint vet
build:
@echo "Building binary..."
@bash ./scripts/build
build-all:
@echo "Building binary..."
@bash ./scripts/build-all
## clean: cleans the binary
clean:
@echo "Cleaning..."
@go clean
## help: prints this help message
help:
@echo "Usage: \n"
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
## lint: lint codebase
lint:
@echo "Linting code..."
@golangci-lint run --fix
## vet: code analysis
vet:
@echo "Running code analysis..."
@go vet ./...

80
client/balance.go Normal file
View File

@@ -0,0 +1,80 @@
package main
import (
"sync"
"github.com/urfave/cli/v2"
)
var balanceCommand = cli.Command{
Name: "balance",
Usage: "Print balance of the Ark wallet",
Action: balanceAction,
}
func balanceAction(ctx *cli.Context) error {
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
offchainAddr, onchainAddr, err := getAddress()
if err != nil {
return err
}
wg := &sync.WaitGroup{}
wg.Add(2)
chRes := make(chan balanceRes, 2)
go func() {
defer wg.Done()
balance, err := getOffchainBalance(ctx, client, offchainAddr)
if err != nil {
chRes <- balanceRes{0, 0, err}
return
}
chRes <- balanceRes{balance, 0, nil}
}()
go func() {
defer wg.Done()
balance, err := getOnchainBalance(onchainAddr)
if err != nil {
chRes <- balanceRes{0, 0, err}
return
}
chRes <- balanceRes{0, balance, nil}
}()
wg.Wait()
offchainBalance, onchainBalance := uint64(0), uint64(0)
count := 0
for res := range chRes {
if res.err != nil {
return res.err
}
if res.offchainBalance > 0 {
offchainBalance = res.offchainBalance
}
if res.onchainBalance > 0 {
onchainBalance = res.onchainBalance
}
count++
if count == 2 {
break
}
}
return printJSON(map[string]interface{}{
"offchain_balance": offchainBalance,
"onchain_balance": onchainBalance,
})
}
type balanceRes struct {
offchainBalance uint64
onchainBalance uint64
err error
}

82
client/client.go Normal file
View File

@@ -0,0 +1,82 @@
package main
import (
"fmt"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"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
}
func getVtxos(
ctx *cli.Context, client arkv1.ArkServiceClient, addr string,
) ([]vtxo, error) {
response, err := client.ListVtxos(ctx.Context, &arkv1.ListVtxosRequest{
Address: addr,
})
if err != nil {
return nil, err
}
vtxos := make([]vtxo, 0, len(response.Vtxos))
for _, v := range response.Vtxos {
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.Txid,
vout: v.Outpoint.Vout,
poolTxid: v.PoolTxid,
})
}
return vtxos, nil
}
func getClientFromState(ctx *cli.Context) (arkv1.ArkServiceClient, func(), error) {
state, err := getState()
if err != nil {
return nil, nil, err
}
addr, ok := state["ark_url"]
if !ok {
return nil, nil, fmt.Errorf("missing ark_url")
}
return getClient(ctx, addr)
}
func getClient(ctx *cli.Context, addr string) (arkv1.ArkServiceClient, func(), error) {
creds := insecure.NewCredentials()
port := 80
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.Dial(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
}

662
client/common.go Normal file
View File

@@ -0,0 +1,662 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"syscall"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"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/address"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"golang.org/x/term"
)
func hashPassword(password []byte) []byte {
hash := sha256.Sum256(password)
return hash[:]
}
func verifyPassword(password []byte) error {
state, err := getState()
if err != nil {
return err
}
passwordHashString, ok := state["password_hash"]
if !ok {
return fmt.Errorf("password hash not found")
}
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
}
func readPassword() ([]byte, error) {
fmt.Print("password: ")
passwordInput, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // new line
if err != nil {
return nil, err
}
err = verifyPassword(passwordInput)
if err != nil {
return nil, err
}
return passwordInput, nil
}
func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
state, err := getState()
if err != nil {
return nil, err
}
encryptedPrivateKeyString, ok := state["encrypted_private_key"]
if !ok {
return nil, fmt.Errorf("encrypted private key not found")
}
encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString)
if err != nil {
return nil, fmt.Errorf("invalid encrypted private key: %s", err)
}
password, err := readPassword()
if err != nil {
return nil, err
}
fmt.Println("key unlocked")
cypher := NewAES128Cypher()
privateKeyBytes, err := cypher.Decrypt(encryptedPrivateKey, password)
if err != nil {
return nil, err
}
privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)
return privateKey, nil
}
func getWalletPublicKey() (*secp256k1.PublicKey, error) {
state, err := getState()
if err != nil {
return nil, err
}
publicKeyString, ok := state["public_key"]
if !ok {
return nil, fmt.Errorf("public key not found")
}
_, publicKey, err := common.DecodePubKey(publicKeyString)
if err != nil {
return nil, err
}
return publicKey, nil
}
func getServiceProviderPublicKey() (*secp256k1.PublicKey, error) {
state, err := getState()
if err != nil {
return nil, err
}
arkPubKey, ok := state["ark_pubkey"]
if !ok {
return nil, fmt.Errorf("ark public key not found")
}
_, pubKey, err := common.DecodePubKey(arkPubKey)
if err != nil {
return nil, err
}
return pubKey, nil
}
func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
selected := make([]vtxo, 0)
selectedAmount := uint64(0)
for _, vtxo := range vtxos {
if selectedAmount >= amount {
break
}
selected = append(selected, vtxo)
selectedAmount += vtxo.amount
}
if selectedAmount < amount {
return nil, 0, fmt.Errorf("insufficient balance: %d to cover %d", selectedAmount, amount)
}
change := selectedAmount - amount
return selected, change, nil
}
func getOffchainBalance(
ctx *cli.Context, client arkv1.ArkServiceClient, addr string,
) (uint64, error) {
vtxos, err := getVtxos(ctx, client, addr)
if err != nil {
return 0, err
}
var balance uint64
for _, vtxo := range vtxos {
balance += vtxo.amount
}
return balance, nil
}
type utxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
Asset string `json:"asset"`
}
func getOnchainUtxos(addr string) ([]utxo, error) {
_, net, err := getNetwork()
if err != nil {
return nil, err
}
baseUrl := explorerUrl[net.Name]
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", baseUrl, addr))
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 := []utxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return payload, nil
}
func getOnchainBalance(addr string) (uint64, error) {
payload, err := getOnchainUtxos(addr)
if err != nil {
return 0, err
}
_, net, err := getNetwork()
if err != nil {
return 0, err
}
balance := uint64(0)
for _, p := range payload {
if p.Asset != net.AssetID {
continue
}
balance += p.Amount
}
return balance, nil
}
func getTxHex(txid string) (string, error) {
_, net, err := getNetwork()
if err != nil {
return "", err
}
baseUrl := explorerUrl[net.Name]
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", baseUrl, txid))
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(body))
}
return string(body), nil
}
func broadcast(txHex string) (string, error) {
_, net, err := getNetwork()
if err != nil {
return "", err
}
body := bytes.NewBuffer([]byte(txHex))
baseUrl := explorerUrl[net.Name]
resp, err := http.Post(fmt.Sprintf("%s/tx", baseUrl), "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 getNetwork() (*common.Network, *network.Network, error) {
state, err := getState()
if err != nil {
return nil, nil, err
}
net, ok := state["network"]
if !ok {
return &common.MainNet, &network.Liquid, nil
}
if net == "testnet" {
return &common.TestNet, &network.Testnet, nil
}
return &common.MainNet, &network.Liquid, nil
}
func getAddress() (offchainAddr, onchainAddr string, err error) {
publicKey, err := getWalletPublicKey()
if err != nil {
return
}
aspPublicKey, err := getServiceProviderPublicKey()
if err != nil {
return
}
arkNet, liquidNet, err := getNetwork()
if err != nil {
return
}
arkAddr, err := common.EncodeAddress(arkNet.Addr, publicKey, aspPublicKey)
if err != nil {
return
}
p2wpkh := payment.FromPublicKey(publicKey, liquidNet, nil)
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil {
return
}
offchainAddr = arkAddr
onchainAddr = liquidAddr
return
}
func printJSON(resp interface{}) error {
jsonBytes, err := json.MarshalIndent(resp, "", "\t")
if err != nil {
return err
}
fmt.Println(string(jsonBytes))
return nil
}
func handleRoundStream(
ctx *cli.Context,
client arkv1.ArkServiceClient,
paymentID string,
vtxosToSign []vtxo,
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, client, pingReq)
}
defer pingStop()
for {
event, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if event.GetRoundFailed() != nil {
pingStop()
return "", fmt.Errorf("round failed: %s", event.GetRoundFailed().GetReason())
}
if event.GetRoundFinalization() != nil {
// stop pinging as soon as we receive some forfeit txs
pingStop()
fmt.Println("round finalization started")
poolPartialTx := event.GetRoundFinalization().GetPoolPartialTx()
poolTransaction, err := psetv2.NewPsetFromBase64(poolPartialTx)
if err != nil {
return "", err
}
congestionTree, err := toCongestionTree(event.GetRoundFinalization().GetCongestionTree())
if err != nil {
return "", err
}
aspPublicKey, err := getServiceProviderPublicKey()
if err != nil {
return "", err
}
_, seconds, err := findSweepClosure(congestionTree)
if err != nil {
return "", err
}
// validate the congestion tree
if err := tree.ValidateCongestionTree(
congestionTree,
poolPartialTx,
aspPublicKey,
int64(seconds),
); err != nil {
return "", err
}
// validate the receivers
sweepLeaf, err := tree.SweepScript(aspPublicKey, seconds)
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 poolTransaction.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
vtxoScript, err := tree.VtxoScript(userPubKey)
if err != nil {
return "", err
}
vtxoTaprootTree := taproot.AssembleTaprootScriptTree(*vtxoScript, *sweepLeaf)
root := vtxoTaprootTree.RootNode.TapHash()
unspendableKey := tree.UnspendableKey()
vtxoTaprootKey := schnorr.SerializePubKey(taproot.ComputeTaprootOutputKey(unspendableKey, root[:]))
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:], vtxoTaprootKey) {
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")
forfeits := event.GetRoundFinalization().GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Println("signing forfeit txs...")
explorer := NewExplorer()
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 {
if err := signPset(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.Println("no forfeit txs to sign, waiting for the next round...")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx, client, pingReq)
}
continue
}
fmt.Printf("%d forfeit txs signed, finalizing payment...\n", len(signedForfeits))
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits,
})
if err != nil {
return "", err
}
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 *cli.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest) func() {
_, err := client.Ping(ctx.Context, 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.Context, req)
}
}(ticker)
return ticker.Stop
}
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 decodeReceiverAddress(addr string) (
isOnChainAddress bool,
onchainScript []byte,
userPubKey *secp256k1.PublicKey,
err error,
) {
outputScript, err := address.ToOutputScript(addr)
if err != nil {
_, userPubKey, _, err = common.DecodeAddress(addr)
if err != nil {
return
}
return false, nil, userPubKey, nil
}
return true, outputScript, nil, nil
}
func findSweepClosure(
congestionTree tree.CongestionTree,
) (sweepClosure *taproot.TapElementsLeaf, seconds uint, err error) {
root, err := congestionTree.Root()
if err != nil {
return
}
// find the sweep closure
tx, err := psetv2.NewPsetFromBase64(root.Tx)
if err != nil {
return
}
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
isSweep, _, lifetime, err := tree.DecodeSweepScript(tapLeaf.Script)
if err != nil {
continue
}
if isSweep {
seconds = lifetime
sweepClosure = &tapLeaf.TapElementsLeaf
break
}
}
if sweepClosure == nil {
return nil, 0, fmt.Errorf("sweep closure not found")
}
return
}

20
client/config.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/urfave/cli/v2"
)
var configCommand = cli.Command{
Name: "config",
Usage: "Print local configuration of the Ark CLI",
Action: printConfigAction,
}
func printConfigAction(ctx *cli.Context) error {
state, err := getState()
if err != nil {
return err
}
return printJSON(state)
}

106
client/cypher.go Normal file
View File

@@ -0,0 +1,106 @@
package main
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():]
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
}

57
client/explorer.go Normal file
View File

@@ -0,0 +1,57 @@
package main
import (
"strings"
"github.com/vulpemventures/go-elements/transaction"
)
type Explorer interface {
GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error)
}
type explorer struct {
cache map[string]string
}
func NewExplorer() Explorer {
return &explorer{
cache: make(map[string]string),
}
}
func (e *explorer) GetTxHex(txid string) (string, error) {
if hex, ok := e.cache[txid]; ok {
return hex, nil
}
txHex, err := getTxHex(txid)
if err != nil {
return "", err
}
e.cache[txid] = txHex
return txHex, nil
}
func (e *explorer) Broadcast(txHex string) (string, error) {
tx, err := transaction.NewTxFromHex(txHex)
if err != nil {
return "", err
}
txid := tx.TxHash().String()
e.cache[txid] = txHex
txid, err = broadcast(txHex)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "transaction already in block chain") {
return txid, nil
}
return "", err
}
return txid, nil
}

71
client/faucet.go Normal file
View File

@@ -0,0 +1,71 @@
package main
import (
"context"
"fmt"
"io"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/urfave/cli/v2"
)
var faucetCommand = cli.Command{
Name: "faucet",
Usage: "Faucet your wallet",
Action: faucetAction,
}
func faucetAction(ctx *cli.Context) error {
addr, _, err := getAddress()
if err != nil {
return err
}
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
_, err = client.Faucet(ctx.Context, &arkv1.FaucetRequest{
Address: addr,
})
if err != nil {
return err
}
eventStream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
if err != nil {
return err
}
for {
event, err := eventStream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
if event.GetRoundFinalization() != nil {
if _, err := client.FinalizePayment(context.Background(), &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: event.GetRoundFinalization().GetForfeitTxs(),
}); err != nil {
return err
}
}
if event.GetRoundFailed() != nil {
return fmt.Errorf("faucet failed: %s", event.GetRoundFailed().GetReason())
}
if event.GetRoundFinalized() != nil {
return printJSON(map[string]interface{}{
"pool_txid": event.GetRoundFinalized().GetPoolTxid(),
})
}
}
return nil
}

44
client/go.mod Normal file
View File

@@ -0,0 +1,44 @@
module github.com/ark-network/ark-cli
go 1.21.0
replace github.com/ark-network/ark/common => ../common
replace github.com/ark-network/ark => ../server
require (
github.com/ark-network/ark v0.0.0-00010101000000-000000000000
github.com/ark-network/ark/common v0.0.0
github.com/btcsuite/btcd v0.23.4
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/urfave/cli/v2 v2.26.0
golang.org/x/crypto v0.16.0
golang.org/x/term v0.15.0
)
require (
github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
)
require (
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/vulpemventures/go-elements v0.5.3
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.31.0 // indirect
)

160
client/go.sum Normal file
View File

@@ -0,0 +1,160 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ=
github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg=
github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3 h1:SDlJ7bAm4ewvrmZtR0DaiYbQGdKPeaaIm7bM+qRhFeU=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/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/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 h1:CTcw80hz/Sw8hqlKX5ZYvBUF5gAHSHwdjXxRf/cjDcI=
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:GXBJykxW2kUcktGdsgyay7uwwWvkljASfljNcT0mbh8=
github.com/vulpemventures/go-elements v0.5.3 h1:zaC/ynHFwCAzFSOMfzb6BcbD6FXASppSiGMycc95WVA=
github.com/vulpemventures/go-elements v0.5.3/go.mod h1:aBGuWXHaiAIUIcwqCdtEh2iQ3kJjKwHU9ywvhlcRSeU=
github.com/vulpemventures/go-secp256k1-zkp v1.1.6 h1:BmsrmXRLUibwa75Qkk8yELjpzCzlAjYFGLiLiOdq7Xo=
github.com/vulpemventures/go-secp256k1-zkp v1.1.6/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 h1:W12Pwm4urIbRdGhMEg2NM9O3TWKjNcxQhs46V0ypf/k=
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 h1:ZcOkrmX74HbKFYnpPY8Qsw93fC29TbJXspYKaBkSXDQ=
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

137
client/init.go Normal file
View File

@@ -0,0 +1,137 @@
package main
import (
"encoding/hex"
"fmt"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"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 (
passwordFlag = cli.StringFlag{
Name: "password",
Usage: "password to encrypt private key",
Required: true,
}
privateKeyFlag = cli.StringFlag{
Name: "prvkey",
Usage: "optional, private key to encrypt",
}
networkFlag = cli.StringFlag{
Name: "network",
Usage: "network to use (mainnet, testnet)",
Value: "testnet",
}
urlFlag = cli.StringFlag{
Name: "ark-url",
Usage: "the url of the ASP to connect to",
Required: true,
}
)
var initCommand = cli.Command{
Name: "init",
Usage: "initialize the wallet with an encryption password, and connect it to an ASP",
Action: initAction,
Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag},
}
func initAction(ctx *cli.Context) error {
key := ctx.String("prvkey")
password := ctx.String("password")
net := strings.ToLower(ctx.String("network"))
url := ctx.String("ark-url")
if len(password) <= 0 {
return fmt.Errorf("invalid password")
}
if len(url) <= 0 {
return fmt.Errorf("invalid ark url")
}
if net != "mainnet" && net != "testnet" {
return fmt.Errorf("invalid network")
}
if err := connectToAsp(ctx, net, url); 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 string) error {
client, close, err := getClient(ctx, url)
if err != nil {
return err
}
defer close()
resp, err := client.GetPubkey(ctx.Context, &arkv1.GetPubkeyRequest{})
if err != nil {
return err
}
return setState(map[string]string{
"ark_url": url,
"network": net,
"ark_pubkey": resp.Pubkey,
})
}
func initWallet(ctx *cli.Context, key, password string) 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 := NewAES128Cypher()
arkNetwork, _, err := getNetwork()
if err != nil {
return err
}
publicKey, err := common.EncodePubKey(arkNetwork.PubKey, privateKey.PubKey())
if err != nil {
return err
}
encryptedPrivateKey, err := cypher.Encrypt(privateKey.Serialize(), []byte(password))
if err != nil {
return err
}
passwordHash := hashPassword([]byte(password))
state := map[string]string{
"encrypted_private_key": hex.EncodeToString(encryptedPrivateKey),
"password_hash": hex.EncodeToString(passwordHash),
"public_key": publicKey,
}
return setState(state)
}

167
client/main.go Normal file
View File

@@ -0,0 +1,167 @@
package main
import (
"encoding/json"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/ark-network/ark/common"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/network"
)
const (
DATADIR_ENVVAR = "ARK_WALLET_DATADIR"
STATE_FILE = "state.json"
defaultNetwork = "testnet"
)
var (
version = "alpha"
datadir = common.AppDataDir("ark-cli", false)
statePath = filepath.Join(datadir, STATE_FILE)
explorerUrl = map[string]string{
network.Liquid.Name: "https://blockstream.info/liquid/api",
network.Testnet.Name: "https://blockstream.info/liquidtestnet/api",
}
initialState = map[string]string{
"ark_url": "",
"ark_pubkey": "",
"encrypted_private_key": "",
"password_hash": "",
"public_key": "",
"network": defaultNetwork,
}
)
func initCLIEnv() {
dir := cleanAndExpandPath(os.Getenv(DATADIR_ENVVAR))
if len(dir) <= 0 {
return
}
datadir = dir
statePath = filepath.Join(datadir, STATE_FILE)
}
func main() {
initCLIEnv()
app := cli.NewApp()
app.Version = version
app.Name = "Ark CLI"
app.Usage = "command line interface for Ark wallet"
app.Commands = append(
app.Commands,
&balanceCommand,
&configCommand,
&faucetCommand,
&initCommand,
&receiveCommand,
&redeemCommand,
&sendCommand,
)
app.Before = func(ctx *cli.Context) error {
if _, err := os.Stat(datadir); os.IsNotExist(err) {
return os.Mkdir(datadir, os.ModeDir|0755)
}
return nil
}
err := app.Run(os.Args)
if err != nil {
fmt.Println(fmt.Errorf("error: %v", err))
os.Exit(1)
}
}
// cleanAndExpandPath expands environment variables and leading ~ in the
// passed path, cleans the result, and returns it.
// This function is taken from https://github.com/btcsuite/btcd
func cleanAndExpandPath(path string) string {
if path == "" {
return ""
}
// Expand initial ~ to OS specific home directory.
if strings.HasPrefix(path, "~") {
var homeDir string
u, err := user.Current()
if err == nil {
homeDir = u.HomeDir
} else {
homeDir = os.Getenv("HOME")
}
path = strings.Replace(path, "~", homeDir, 1)
}
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
// but the variables can still be expanded via POSIX-style $VARIABLE.
return filepath.Clean(os.ExpandEnv(path))
}
func getState() (map[string]string, error) {
file, err := os.ReadFile(statePath)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err := setInitialState(); 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 setInitialState() error {
jsonString, err := json.Marshal(initialState)
if err != nil {
return err
}
return os.WriteFile(statePath, jsonString, 0755)
}
func setState(data map[string]string) error {
currentData, err := getState()
if err != nil {
return err
}
mergedData := merge(currentData, data)
jsonString, err := json.Marshal(mergedData)
if err != nil {
return err
}
err = os.WriteFile(statePath, jsonString, 0755)
if err != nil {
return fmt.Errorf("writing to file: %w", err)
}
return 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
}

29
client/receive.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"github.com/urfave/cli/v2"
)
var receiveCommand = cli.Command{
Name: "receive",
Usage: "Print the Ark address associated with your wallet and the connected Ark",
Action: receiveAction,
}
func receiveAction(ctx *cli.Context) error {
offchainAddr, onchainAddr, err := getAddress()
if err != nil {
return err
}
state, err := getState()
if err != nil {
return err
}
relays := []string{state["ark_url"]}
return printJSON(map[string]interface{}{
"offchain_address": offchainAddr,
"onchain_address": onchainAddr,
"relays": relays,
})
}

391
client/redeem.go Normal file
View File

@@ -0,0 +1,391 @@
package main
import (
"bufio"
"fmt"
"log"
"math"
"os"
"strings"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/psetv2"
)
var (
addressFlag = cli.StringFlag{
Name: "address",
Usage: "main chain address receiving the redeeemed VTXO",
Value: "",
Required: true,
}
amountToRedeemFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to redeem",
Value: 0,
Required: false,
}
forceFlag = cli.BoolFlag{
Name: "force",
Usage: "force redemption without collaborate with the Ark service provider",
Value: false,
Required: false,
}
)
var redeemCommand = cli.Command{
Name: "redeem",
Usage: "Redeem VTXO(s) to onchain",
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag},
Action: redeemAction,
}
func redeemAction(ctx *cli.Context) error {
addr := ctx.String("address")
amount := ctx.Uint64("amount")
force := ctx.Bool("force")
if len(addr) <= 0 {
return fmt.Errorf("missing address flag (--address)")
}
if _, err := address.ToOutputScript(addr); err != nil {
return fmt.Errorf("invalid onchain address")
}
net, err := address.NetworkForAddress(addr)
if err != nil {
return fmt.Errorf("invalid onchain address: unknown network")
}
_, liquidNet, _ := getNetwork()
if net.Name != liquidNet.Name {
return fmt.Errorf("invalid onchain address: must be for %s network", liquidNet.Name)
}
if !force && amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
if force {
if amount > 0 {
fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n")
}
return unilateralRedeem(ctx, addr)
}
return collaborativeRedeem(ctx, addr, amount)
}
func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
if isConf, _ := address.IsConfidential(addr); isConf {
info, _ := address.FromConfidential(addr)
addr = info.Address
}
offchainAddr, _, err := getAddress()
if err != nil {
return err
}
receivers := []*arkv1.Output{
{
Address: addr,
Amount: amount,
},
}
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
vtxos, err := getVtxos(ctx, client, offchainAddr)
if err != nil {
return err
}
selectedCoins, changeAmount, err := coinSelect(vtxos, amount)
if err != nil {
return err
}
if changeAmount > 0 {
receivers = append(receivers, &arkv1.Output{
Address: offchainAddr,
Amount: changeAmount,
})
}
inputs := make([]*arkv1.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
})
}
secKey, err := privateKeyFromPassword()
if err != nil {
return err
}
registerResponse, err := client.RegisterPayment(ctx.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,
secKey,
receivers,
)
if err != nil {
return err
}
if err := printJSON(map[string]interface{}{
"pool_txid": poolTxID,
}); err != nil {
return err
}
return nil
}
func unilateralRedeem(ctx *cli.Context, addr string) error {
onchainScript, err := address.ToOutputScript(addr)
if err != nil {
return err
}
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
offchainAddr, _, err := getAddress()
if err != nil {
return err
}
vtxos, err := getVtxos(ctx, client, offchainAddr)
if err != nil {
return err
}
totalVtxosAmount := uint64(0)
for _, vtxo := range vtxos {
totalVtxosAmount += vtxo.amount
}
ok := askForConfirmation(fmt.Sprintf("redeem %d sats to %s ?", totalVtxosAmount, addr))
if !ok {
return fmt.Errorf("aborting unilateral exit")
}
finalPset, err := psetv2.New(nil, nil, nil)
if err != nil {
return err
}
updater, err := psetv2.NewUpdater(finalPset)
if err != nil {
return err
}
congestionTrees := make(map[string]tree.CongestionTree, 0)
transactionsMap := make(map[string]struct{}, 0)
transactions := make([]string, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
round, err := client.GetRound(ctx.Context, &arkv1.GetRoundRequest{
Txid: vtxo.poolTxid,
})
if err != nil {
return err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return err
}
congestionTrees[vtxo.poolTxid] = congestionTree
}
redeemBranch, err := newRedeemBranch(ctx, congestionTrees[vtxo.poolTxid], vtxo)
if err != nil {
return err
}
if err := redeemBranch.UpdatePath(); err != nil {
return err
}
branchTxs, err := redeemBranch.RedeemPath()
if err != nil {
return err
}
if err := redeemBranch.AddVtxoInput(updater); err != nil {
return err
}
for _, txHex := range branchTxs {
if _, ok := transactionsMap[txHex]; !ok {
transactions = append(transactions, txHex)
transactionsMap[txHex] = struct{}{}
}
}
}
_, net, err := getNetwork()
if err != nil {
return err
}
outputs := []psetv2.OutputArgs{
{
Asset: net.AssetID,
Amount: totalVtxosAmount,
Script: onchainScript,
},
}
if err := updater.AddOutputs(outputs); err != nil {
return err
}
utx, err := updater.Pset.UnsignedTx()
if err != nil {
return err
}
vBytes := utx.VirtualSize()
feeAmount := uint64(math.Ceil(float64(vBytes) * 0.25))
if totalVtxosAmount-feeAmount <= 0 {
return fmt.Errorf("not enough VTXOs to pay the fees (%d sats), aborting unilateral exit", feeAmount)
}
updater.Pset.Outputs[0].Value = totalVtxosAmount - feeAmount
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: net.AssetID,
Amount: feeAmount,
},
}); err != nil {
return err
}
prvKey, err := privateKeyFromPassword()
if err != nil {
return err
}
explorer := NewExplorer()
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
}
}
}
if err := signPset(finalPset, explorer, prvKey); err != nil {
return err
}
for i, input := range finalPset.Inputs {
if len(input.TapScriptSig) > 0 || len(input.PartialSigs) > 0 {
if err := psetv2.Finalize(finalPset, i); err != nil {
return err
}
}
}
signedTx, err := psetv2.Extract(finalPset)
if err != nil {
return err
}
hex, err := signedTx.ToHex()
if err != nil {
return err
}
for {
id, err := explorer.Broadcast(hex)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "bad-txns-inputs-missingorspent") {
time.Sleep(1 * time.Second)
continue
}
return err
}
if id != "" {
fmt.Printf("(final) redeem tx %s\n", id)
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
}
}
}

16
client/scripts/build Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -e
PARENT_PATH=$(dirname $(
cd $(dirname $0)
pwd -P
))
OS=$(eval "go env GOOS")
ARCH=$(eval "go env GOARCH")
pushd $PARENT_PATH
mkdir -p build
GO111MODULE=on go build -ldflags="-s -w" -o build/ark-cli-$OS-$ARCH .
popd

23
client/scripts/build-all Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
set -e
PARENT_PATH=$(dirname $(
cd $(dirname $0)
pwd -P
))
declare -a OS=("darwin" "linux")
declare -a ARCH=("amd64" "arm64")
pushd $PARENT_PATH
mkdir -p build
for os in "${OS[@]}"; do
for arch in "${ARCH[@]}"; do
echo "Building for $os $arch"
GOOS=$os GOARCH=$arch go build -o build/ark-$os-$arch .
done
done
popd

171
client/send.go Normal file
View File

@@ -0,0 +1,171 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common"
"github.com/urfave/cli/v2"
)
type receiver struct {
To string `json:"to"`
Amount uint64 `json:"amount"`
}
var (
receiversFlag = cli.StringFlag{
Name: "receivers",
Usage: "receivers of the send transaction, JSON encoded: '[{\"to\": \"<...>\", \"amount\": <...>}, ...]'",
}
toFlag = cli.StringFlag{
Name: "to",
Usage: "ark address of the recipient",
}
amountFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to send in sats",
}
)
var sendCommand = cli.Command{
Name: "send",
Usage: "Send VTXOs to a list of addresses",
Action: sendAction,
Flags: []cli.Flag{&receiversFlag, &toFlag, &amountFlag},
}
func sendAction(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")
}
offchainAddr, _, err := getAddress()
if err != nil {
return err
}
_, _, aspPubKey, err := common.DecodeAddress(offchainAddr)
if err != nil {
return err
}
receiversOutput := make([]*arkv1.Output, 0)
sumOfReceivers := uint64(0)
for _, receiver := range receiversJSON {
_, _, 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 <= 0 {
return fmt.Errorf("invalid amount: %d", receiver.Amount)
}
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()
vtxos, err := getVtxos(ctx, client, offchainAddr)
if err != nil {
return err
}
selectedCoins, changeAmount, err := coinSelect(vtxos, sumOfReceivers)
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{
Txid: coin.txid,
Vout: coin.vout,
})
}
secKey, err := privateKeyFromPassword()
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,
secKey,
receiversOutput,
)
if err != nil {
return err
}
if err := printJSON(map[string]interface{}{
"pool_txid": poolTxID,
}); err != nil {
return err
}
return nil
}

194
client/signer.go Normal file
View File

@@ -0,0 +1,194 @@
package main
import (
"bytes"
"fmt"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"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/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
func signPset(
pset *psetv2.Pset,
explorer 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
}
sighashType := txscript.SigHashAll
if utxo.Script[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(i, sighashType); err != nil {
return err
}
}
signer, err := psetv2.NewSigner(updater.Pset)
if err != nil {
return err
}
_, onchainAddr, err := getAddress()
if err != nil {
return err
}
onchainWalletScript, err := address.ToOutputScript(onchainAddr)
if err != nil {
return err
}
utx, err := pset.UnsignedTx()
if err != nil {
return err
}
_, liquidNet, err := getNetwork()
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)
}
for i, input := range pset.Inputs {
if bytes.Equal(input.WitnessUtxo.Script, onchainWalletScript) {
p, err := payment.FromScript(input.WitnessUtxo.Script, liquidNet, nil)
if err != nil {
return err
}
preimage := utx.HashForWitnessV0(
i,
p.Script,
input.WitnessUtxo.Value,
txscript.SigHashAll,
)
sig := ecdsa.Sign(
prvKey,
preimage[:],
)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
err = signer.SignInput(i, signatureWithSighashType, prvKey.PubKey().SerializeCompressed(), nil, nil)
if err != nil {
fmt.Println("error signing input: ", err)
return err
}
continue
}
pubkey := prvKey.PubKey()
vtxoLeaf, err := tree.VtxoScript(pubkey)
if err != nil {
return err
}
if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil {
return err
}
for _, leaf := range input.TapLeafScript {
if bytes.Equal(leaf.Script, vtxoLeaf.Script) {
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
}

185
client/unilateral_redeem.go Normal file
View File

@@ -0,0 +1,185 @@
package main
import (
"fmt"
"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 interface {
// UpdatePath checks for transactions of the branch onchain and updates the branch accordingly
UpdatePath() error
// Redeem will sign the branch of the tree and return the associated signed pset + the vtxo input
RedeemPath() ([]string, error)
// AddInput adds the vtxo input created by the branch
AddVtxoInput(updater *psetv2.Updater) error
}
type redeemBranch struct {
vtxo *vtxo
branch []*psetv2.Pset
internalKey *secp256k1.PublicKey
sweepClosure *taproot.TapElementsLeaf
}
func newRedeemBranch(ctx *cli.Context, congestionTree tree.CongestionTree, vtxo vtxo) (RedeemBranch, error) {
sweepClosure, _, err := findSweepClosure(congestionTree)
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,
}, nil
}
// UpdatePath checks for transactions of the branch onchain and updates the branch accordingly
func (r *redeemBranch) UpdatePath() error {
for i := len(r.branch) - 1; i >= 0; i-- {
pset := r.branch[i]
unsignedTx, err := pset.UnsignedTx()
if err != nil {
return err
}
txHash := unsignedTx.TxHash().String()
_, err = 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 {
r.branch = []*psetv2.Pset{}
} else {
r.branch = r.branch[i+1:]
}
break
}
return 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))
for _, pset := range r.branch {
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 {
isSweep, _, _, err := tree.DecodeSweepScript(leaf.Script)
if err != nil {
return nil, err
}
if isSweep {
continue
}
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)
break
}
}
}
return transactions, nil
}
// AddVtxoInput is a wrapper around psetv2.Updater adding a taproot input letting to spend the vtxo output
func (r *redeemBranch) AddVtxoInput(updater *psetv2.Updater) error {
walletPubkey, err := getWalletPublicKey()
if err != nil {
return err
}
nextInputIndex := len(updater.Pset.Inputs)
if err := updater.AddInputs([]psetv2.InputArgs{
{
Txid: r.vtxo.txid,
TxIndex: r.vtxo.vout,
},
}); err != nil {
return err
}
// add taproot tree letting to spend the vtxo
checksigLeaf, err := tree.VtxoScript(walletPubkey)
if err != nil {
return nil
}
vtxoTaprootTree := taproot.AssembleTaprootScriptTree(
*checksigLeaf,
*r.sweepClosure,
)
proofIndex := vtxoTaprootTree.LeafProofIndex[checksigLeaf.TapHash()]
if err := updater.AddInTapLeafScript(
nextInputIndex,
psetv2.NewTapLeafScript(
vtxoTaprootTree.LeafMerkleProofs[proofIndex],
r.internalKey,
),
); err != nil {
return err
}
return nil
}