mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-22 05:04:20 +01:00
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:
committed by
GitHub
parent
0d8c7bffb2
commit
dc00d60585
1
client/.gitignore
vendored
Normal file
1
client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build/
|
||||
30
client/Makefile
Normal file
30
client/Makefile
Normal 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
80
client/balance.go
Normal 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
82
client/client.go
Normal 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
662
client/common.go
Normal 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
20
client/config.go
Normal 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
106
client/cypher.go
Normal 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
57
client/explorer.go
Normal 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
71
client/faucet.go
Normal 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
44
client/go.mod
Normal 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
160
client/go.sum
Normal 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
137
client/init.go
Normal 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
167
client/main.go
Normal 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
29
client/receive.go
Normal 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
391
client/redeem.go
Normal 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
16
client/scripts/build
Executable 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
23
client/scripts/build-all
Executable 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
171
client/send.go
Normal 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
194
client/signer.go
Normal 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
185
client/unilateral_redeem.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user