Covenant-less ark sdk (#225)

* Add method to compute vtxo taproot script

* Drop debug logs

* Refactor client:
* Remove dep from stubs in interface
* Remove redeem branch, out of scope (moved to ark client)

* Add example for covenant and covenantless sdk

* Simplify explorer - No need for bitcoin and liquid impls

* Refactor wallet:
* wallet struct for common operations (create, lock/unlock, getType, isLocked)
* liquidWallet struct for liquid operations (derive/get addresses, sign tx)
* bitcoinWallet struct for bitcoin operations (derive/get addresses, sign tx)

* Update utils:
* drop methods to parse tree (moved to ark client)
* add methods for encryption, network parsing, key generation
* add methods for covenant/covenantless redeem branches (move to common?)

* Add support for covenantless sdk:
* move interface to dedicated file
* arkCLient struct for common operations (Init, lock/unlock, get config data, receive)
* covenantArkClient struct for covenant operations (onboard, balance, send, redeem)
* covenantlessArkClient struct for covenantless operations (onboard, balance, send, redeem)

* Fix wasm

* Fixes

* Make explorer use utils.Cache

* Renamings

* Lint

* Fix e2e tests

* Fix e2e test
This commit is contained in:
Pietralberto Mazza
2024-08-07 01:37:18 +02:00
committed by GitHub
parent 1c67c56d9d
commit 8de2df3d7f
39 changed files with 5655 additions and 2747 deletions

View File

@@ -141,6 +141,44 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return valid, nil
}
func ComputeVtxoTaprootScript(
userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint,
) (*secp256k1.PublicKey, *txscript.TapscriptProof, error) {
redeemClosure := &CSVSigClosure{
Pubkey: userPubkey,
Seconds: exitDelay,
}
forfeitClosure := &ForfeitClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, err
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, err
}
vtxoTaprootTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := vtxoTaprootTree.RootNode.TapHash()
unspendableKey := UnspendableKey()
vtxoTaprootKey := txscript.ComputeTaprootOutputKey(unspendableKey, root[:])
redeemLeafHash := redeemLeaf.TapHash()
proofIndex := vtxoTaprootTree.LeafProofIndex[redeemLeafHash]
proof := vtxoTaprootTree.LeafMerkleProofs[proofIndex]
return vtxoTaprootKey, &proof, nil
}
func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
if data32Index == -1 {

File diff suppressed because it is too large Load Diff

282
pkg/client-sdk/client.go Normal file
View File

@@ -0,0 +1,282 @@
package arksdk
import (
"context"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
"github.com/ark-network/ark-sdk/store"
"github.com/ark-network/ark-sdk/wallet"
singlekeywallet "github.com/ark-network/ark-sdk/wallet/singlekey"
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
filestore "github.com/ark-network/ark-sdk/wallet/singlekey/store/file"
inmemorystore "github.com/ark-network/ark-sdk/wallet/singlekey/store/inmemory"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
const (
DUST = 450
// transport
GrpcClient = client.GrpcClient
RestClient = client.RestClient
// wallet
SingleKeyWallet = wallet.SingleKeyWallet
// store
FileStore = store.FileStore
InMemoryStore = store.InMemoryStore
// explorer
BitcoinExplorer = explorer.BitcoinExplorer
LiquidExplorer = explorer.LiquidExplorer
)
var (
ErrAlreadyInitialized = fmt.Errorf("client already initialized")
ErrNotInitialized = fmt.Errorf("client not initialized")
)
type arkClient struct {
*store.StoreData
wallet wallet.WalletService
store store.ConfigStore
explorer explorer.Explorer
client client.ASPClient
}
func (a *arkClient) GetConfigData(
_ context.Context,
) (*store.StoreData, error) {
if a.StoreData == nil {
return nil, fmt.Errorf("client sdk not initialized")
}
return a.StoreData, nil
}
func (a *arkClient) InitWithWallet(
ctx context.Context, args InitWithWalletArgs,
) error {
if err := args.validate(); err != nil {
return fmt.Errorf("invalid args: %s", err)
}
clientSvc, err := getClient(
supportedClients, args.ClientType, args.AspUrl,
)
if err != nil {
return fmt.Errorf("failed to setup client: %s", err)
}
info, err := clientSvc.GetInfo(ctx)
if err != nil {
return fmt.Errorf("failed to connect to asp: %s", err)
}
explorerSvc, err := getExplorer(supportedNetworks, info.Network)
if err != nil {
return fmt.Errorf("failed to setup explorer: %s", err)
}
network := utils.NetworkFromString(info.Network)
buf, err := hex.DecodeString(info.Pubkey)
if err != nil {
return fmt.Errorf("failed to parse asp pubkey: %s", err)
}
aspPubkey, err := secp256k1.ParsePubKey(buf)
if err != nil {
return fmt.Errorf("failed to parse asp pubkey: %s", err)
}
storeData := store.StoreData{
AspUrl: args.AspUrl,
AspPubkey: aspPubkey,
WalletType: args.Wallet.GetType(),
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
MinRelayFee: uint64(info.MinRelayFee),
}
if err := a.store.AddData(ctx, storeData); err != nil {
return err
}
if _, err := args.Wallet.Create(ctx, args.Password, args.Seed); err != nil {
//nolint:all
a.store.CleanData(ctx)
return err
}
a.StoreData = &storeData
a.wallet = args.Wallet
a.explorer = explorerSvc
a.client = clientSvc
return nil
}
func (a *arkClient) Init(
ctx context.Context, args InitArgs,
) error {
if err := args.validate(); err != nil {
return fmt.Errorf("invalid args: %s", err)
}
clientSvc, err := getClient(
supportedClients, args.ClientType, args.AspUrl,
)
if err != nil {
return fmt.Errorf("failed to setup client: %s", err)
}
info, err := clientSvc.GetInfo(ctx)
if err != nil {
return fmt.Errorf("failed to connect to asp: %s", err)
}
explorerSvc, err := getExplorer(supportedNetworks, info.Network)
if err != nil {
return fmt.Errorf("failed to setup explorer: %s", err)
}
network := utils.NetworkFromString(info.Network)
buf, err := hex.DecodeString(info.Pubkey)
if err != nil {
return fmt.Errorf("failed to parse asp pubkey: %s", err)
}
aspPubkey, err := secp256k1.ParsePubKey(buf)
if err != nil {
return fmt.Errorf("failed to parse asp pubkey: %s", err)
}
storeData := store.StoreData{
AspUrl: args.AspUrl,
AspPubkey: aspPubkey,
WalletType: args.WalletType,
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
MinRelayFee: uint64(info.MinRelayFee),
}
walletSvc, err := getWallet(a.store, &storeData, supportedWallets)
if err != nil {
return err
}
if err := a.store.AddData(ctx, storeData); err != nil {
return err
}
if _, err := walletSvc.Create(ctx, args.Password, args.Seed); err != nil {
//nolint:all
a.store.CleanData(ctx)
return err
}
a.StoreData = &storeData
a.wallet = walletSvc
a.explorer = explorerSvc
a.client = clientSvc
return nil
}
func (a *arkClient) Unlock(ctx context.Context, pasword string) error {
_, err := a.wallet.Unlock(ctx, pasword)
return err
}
func (a *arkClient) Lock(ctx context.Context, pasword string) error {
return a.wallet.Lock(ctx, pasword)
}
func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", "", err
}
return offchainAddr, onchainAddr, nil
}
func (a *arkClient) ping(
ctx context.Context, paymentID string,
) func() {
_, err := a.client.Ping(ctx, paymentID)
if err != nil {
return nil
}
ticker := time.NewTicker(5 * time.Second)
go func(t *time.Ticker) {
for range t.C {
// nolint
a.client.Ping(ctx, paymentID)
}
}(ticker)
return ticker.Stop
}
func getClient(
supportedClients utils.SupportedType[utils.ClientFactory], clientType, aspUrl string,
) (client.ASPClient, error) {
factory := supportedClients[clientType]
return factory(aspUrl)
}
func getExplorer(
supportedNetworks utils.SupportedType[string], network string,
) (explorer.Explorer, error) {
url, ok := supportedNetworks[network]
if !ok {
return nil, fmt.Errorf("invalid network")
}
return explorer.NewExplorer(url, utils.NetworkFromString(network)), nil
}
func getWallet(
storeSvc store.ConfigStore, data *store.StoreData, supportedWallets utils.SupportedType[struct{}],
) (wallet.WalletService, error) {
switch data.WalletType {
case wallet.SingleKeyWallet:
return getSingleKeyWallet(storeSvc, data.Network.Name)
default:
return nil, fmt.Errorf(
"unsuported wallet type '%s', please select one of: %s",
data.WalletType, supportedWallets,
)
}
}
func getSingleKeyWallet(
configStore store.ConfigStore, network string,
) (wallet.WalletService, error) {
walletStore, err := getWalletStore(configStore.GetType(), configStore.GetDatadir())
if err != nil {
return nil, err
}
if strings.Contains(network, "liquid") {
return singlekeywallet.NewLiquidWallet(configStore, walletStore)
}
return singlekeywallet.NewBitcoinWallet(configStore, walletStore)
}
func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error) {
switch storeType {
case store.InMemoryStore:
return inmemorystore.NewWalletStore()
case store.FileStore:
return filestore.NewWalletStore(datadir)
default:
return nil, fmt.Errorf("unknown wallet store type")
}
}

View File

@@ -4,8 +4,7 @@ import (
"context"
"time"
"github.com/ark-network/ark-sdk/explorer"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
)
const (
@@ -13,48 +12,121 @@ const (
RestClient = "rest"
)
type RoundEvent interface {
isRoundEvent()
}
type ASPClient interface {
GetInfo(ctx context.Context) (*Info, error)
ListVtxos(ctx context.Context, addr string) ([]Vtxo, []Vtxo, error)
GetRound(ctx context.Context, txID string) (*Round, error)
GetRoundByID(ctx context.Context, roundID string) (*Round, error)
Onboard(
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error
RegisterPayment(
ctx context.Context, inputs []VtxoKey,
) (string, error)
ClaimPayment(
ctx context.Context, paymentID string, outputs []Output,
) error
GetEventStream(
ctx context.Context, paymentID string,
) (<-chan RoundEventChannel, error)
Ping(ctx context.Context, paymentID string) (*RoundFinalizationEvent, error)
FinalizePayment(
ctx context.Context, signedForfeitTxs []string,
) error
Close()
}
type Info struct {
Pubkey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
}
type RoundEventChannel struct {
Event *arkv1.GetEventStreamResponse
Event RoundEvent
Err error
}
type VtxoKey struct {
Txid string
VOut uint32
}
type Vtxo struct {
VtxoKey
Amount uint64
Txid string
VOut uint32
RoundTxid string
ExpiresAt *time.Time
}
type ASPClient interface {
GetInfo(ctx context.Context) (*arkv1.GetInfoResponse, error)
ListVtxos(ctx context.Context, addr string) (*arkv1.ListVtxosResponse, error)
GetSpendableVtxos(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) ([]*Vtxo, error)
GetRound(ctx context.Context, txID string) (*arkv1.GetRoundResponse, error)
GetRoundByID(ctx context.Context, roundID string) (*arkv1.GetRoundByIdResponse, error)
GetRedeemBranches(
ctx context.Context, vtxos []*Vtxo, explorerSvc explorer.Explorer,
) (map[string]*RedeemBranch, error)
GetOffchainBalance(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) (uint64, map[int64]uint64, error)
Onboard(
ctx context.Context, req *arkv1.OnboardRequest,
) (*arkv1.OnboardResponse, error)
RegisterPayment(
ctx context.Context, req *arkv1.RegisterPaymentRequest,
) (*arkv1.RegisterPaymentResponse, error)
ClaimPayment(
ctx context.Context, req *arkv1.ClaimPaymentRequest,
) (*arkv1.ClaimPaymentResponse, error)
GetEventStream(
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
) (<-chan RoundEventChannel, error)
Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.PingResponse, error)
FinalizePayment(
ctx context.Context, req *arkv1.FinalizePaymentRequest,
) (*arkv1.FinalizePaymentResponse, error)
Close()
type Output struct {
Address string
Amount uint64
}
type RoundStage int
func (s RoundStage) String() string {
switch s {
case RoundStageRegistration:
return "ROUND_STAGE_REGISTRATION"
case RoundStageFinalization:
return "ROUND_STAGE_FINALIZATION"
case RoundStageFinalized:
return "ROUND_STAGE_FINALIZED"
case RoundStageFailed:
return "ROUND_STAGE_FAILED"
default:
return "ROUND_STAGE_UNDEFINED"
}
}
const (
RoundStageUndefined RoundStage = iota
RoundStageRegistration
RoundStageFinalization
RoundStageFinalized
RoundStageFailed
)
type Round struct {
ID string
StartedAt *time.Time
EndedAt *time.Time
Tx string
Tree tree.CongestionTree
ForfeitTxs []string
Connectors []string
Stage RoundStage
}
type RoundFinalizationEvent struct {
ID string
Tx string
ForfeitTxs []string
Tree tree.CongestionTree
Connectors []string
}
func (e RoundFinalizationEvent) isRoundEvent() {}
type RoundFinalizedEvent struct {
ID string
Txid string
}
func (e RoundFinalizedEvent) isRoundEvent() {}
type RoundFailedEvent struct {
ID string
Reason string
}
func (e RoundFailedEvent) isRoundEvent() {}

View File

@@ -7,7 +7,7 @@ import (
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
"google.golang.org/grpc"
@@ -16,9 +16,10 @@ import (
)
type grpcClient struct {
conn *grpc.ClientConn
svc arkv1.ArkServiceClient
eventsCh chan client.RoundEventChannel
conn *grpc.ClientConn
svc arkv1.ArkServiceClient
eventsCh chan client.RoundEventChannel
treeCache *utils.Cache[tree.CongestionTree]
}
func NewClient(aspUrl string) (client.ASPClient, error) {
@@ -43,8 +44,9 @@ func NewClient(aspUrl string) (client.ASPClient, error) {
svc := arkv1.NewArkServiceClient(conn)
eventsCh := make(chan client.RoundEventChannel)
treeCache := utils.NewCache[tree.CongestionTree]()
return &grpcClient{conn, svc, eventsCh}, nil
return &grpcClient{conn, svc, eventsCh, treeCache}, nil
}
func (c *grpcClient) Close() {
@@ -53,8 +55,9 @@ func (c *grpcClient) Close() {
}
func (a *grpcClient) GetEventStream(
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
ctx context.Context, paymentID string,
) (<-chan client.RoundEventChannel, error) {
req := &arkv1.GetEventStreamRequest{}
stream, err := a.svc.GetEventStream(ctx, req)
if err != nil {
return nil, err
@@ -70,187 +73,265 @@ func (a *grpcClient) GetEventStream(
return
}
a.eventsCh <- client.RoundEventChannel{Event: resp}
a.eventsCh <- client.RoundEventChannel{Event: event{resp}.toRoundEvent()}
}
}()
return a.eventsCh, nil
}
func (a *grpcClient) GetInfo(ctx context.Context) (*arkv1.GetInfoResponse, error) {
return a.svc.GetInfo(ctx, &arkv1.GetInfoRequest{})
func (a *grpcClient) GetInfo(ctx context.Context) (*client.Info, error) {
req := &arkv1.GetInfoRequest{}
resp, err := a.svc.GetInfo(ctx, req)
if err != nil {
return nil, err
}
return &client.Info{
Pubkey: resp.GetPubkey(),
RoundLifetime: resp.GetRoundLifetime(),
UnilateralExitDelay: resp.GetUnilateralExitDelay(),
RoundInterval: resp.GetRoundInterval(),
Network: resp.GetNetwork(),
MinRelayFee: resp.GetMinRelayFee(),
}, nil
}
func (a *grpcClient) ListVtxos(
ctx context.Context,
addr string,
) (*arkv1.ListVtxosResponse, error) {
return a.svc.ListVtxos(ctx, &arkv1.ListVtxosRequest{Address: addr})
ctx context.Context, addr string,
) ([]client.Vtxo, []client.Vtxo, error) {
resp, err := a.svc.ListVtxos(ctx, &arkv1.ListVtxosRequest{Address: addr})
if err != nil {
return nil, nil, err
}
return vtxos(resp.GetSpendableVtxos()).toVtxos(), vtxos(resp.GetSpentVtxos()).toVtxos(), nil
}
func (a *grpcClient) GetRound(
ctx context.Context, txID string,
) (*arkv1.GetRoundResponse, error) {
return a.svc.GetRound(ctx, &arkv1.GetRoundRequest{Txid: txID})
}
func (a *grpcClient) GetSpendableVtxos(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) ([]*client.Vtxo, error) {
allVtxos, err := a.ListVtxos(ctx, addr)
) (*client.Round, error) {
req := &arkv1.GetRoundRequest{Txid: txID}
resp, err := a.svc.GetRound(ctx, req)
if err != nil {
return nil, err
}
vtxos := make([]*client.Vtxo, 0, len(allVtxos.GetSpendableVtxos()))
for _, v := range allVtxos.GetSpendableVtxos() {
var expireAt *time.Time
if v.ExpireAt > 0 {
t := time.Unix(v.ExpireAt, 0)
expireAt = &t
}
if v.Swept {
continue
}
vtxos = append(vtxos, &client.Vtxo{
Amount: v.Receiver.Amount,
Txid: v.Outpoint.Txid,
VOut: v.Outpoint.Vout,
RoundTxid: v.PoolTxid,
ExpiresAt: expireAt,
})
round := resp.GetRound()
startedAt := time.Unix(round.GetStart(), 0)
var endedAt *time.Time
if round.GetEnd() > 0 {
t := time.Unix(round.GetEnd(), 0)
endedAt = &t
}
if explorerSvc == nil {
return vtxos, nil
}
redeemBranches, err := a.GetRedeemBranches(ctx, vtxos, explorerSvc)
if err != nil {
return nil, err
}
for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpiresAt()
if err != nil {
return nil, err
}
for i, vtxo := range vtxos {
if vtxo.Txid == vtxoTxid {
vtxos[i].ExpiresAt = expiration
break
}
}
}
return vtxos, nil
}
func (a *grpcClient) GetRedeemBranches(
ctx context.Context, vtxos []*client.Vtxo, explorerSvc explorer.Explorer,
) (map[string]*client.RedeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0)
redeemBranches := make(map[string]*client.RedeemBranch, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.RoundTxid]; !ok {
round, err := a.GetRound(ctx, vtxo.RoundTxid)
if err != nil {
return nil, err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return nil, err
}
congestionTrees[vtxo.RoundTxid] = congestionTree
}
redeemBranch, err := client.NewRedeemBranch(
explorerSvc, congestionTrees[vtxo.RoundTxid], vtxo,
)
if err != nil {
return nil, err
}
redeemBranches[vtxo.Txid] = redeemBranch
}
return redeemBranches, nil
}
func (a *grpcClient) GetOffchainBalance(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0)
vtxos, err := a.GetSpendableVtxos(ctx, addr, explorerSvc)
if err != nil {
return 0, nil, err
}
var balance uint64
for _, vtxo := range vtxos {
balance += vtxo.Amount
if vtxo.ExpiresAt != nil {
expiration := vtxo.ExpiresAt.Unix()
if _, ok := amountByExpiration[expiration]; !ok {
amountByExpiration[expiration] = 0
}
amountByExpiration[expiration] += vtxo.Amount
}
}
return balance, amountByExpiration, nil
return &client.Round{
ID: round.GetId(),
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: round.GetPoolTx(),
Tree: treeFromProto{round.GetCongestionTree()}.parse(),
ForfeitTxs: round.GetForfeitTxs(),
Connectors: round.GetConnectors(),
Stage: client.RoundStage(int(round.GetStage())),
}, nil
}
func (a *grpcClient) Onboard(
ctx context.Context, req *arkv1.OnboardRequest,
) (*arkv1.OnboardResponse, error) {
return a.svc.Onboard(ctx, req)
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error {
req := &arkv1.OnboardRequest{
BoardingTx: tx,
UserPubkey: userPubkey,
CongestionTree: treeToProto(congestionTree).parse(),
}
_, err := a.svc.Onboard(ctx, req)
return err
}
func (a *grpcClient) RegisterPayment(
ctx context.Context, req *arkv1.RegisterPaymentRequest,
) (*arkv1.RegisterPaymentResponse, error) {
return a.svc.RegisterPayment(ctx, req)
ctx context.Context, inputs []client.VtxoKey,
) (string, error) {
req := &arkv1.RegisterPaymentRequest{
Inputs: ins(inputs).toProto(),
}
resp, err := a.svc.RegisterPayment(ctx, req)
if err != nil {
return "", err
}
return resp.GetId(), nil
}
func (a *grpcClient) ClaimPayment(
ctx context.Context, req *arkv1.ClaimPaymentRequest,
) (*arkv1.ClaimPaymentResponse, error) {
return a.svc.ClaimPayment(ctx, req)
ctx context.Context, paymentID string, outputs []client.Output,
) error {
req := &arkv1.ClaimPaymentRequest{
Id: paymentID,
Outputs: outs(outputs).toProto(),
}
_, err := a.svc.ClaimPayment(ctx, req)
return err
}
func (a *grpcClient) Ping(
ctx context.Context, req *arkv1.PingRequest,
) (*arkv1.PingResponse, error) {
return a.svc.Ping(ctx, req)
ctx context.Context, paymentID string,
) (*client.RoundFinalizationEvent, error) {
req := &arkv1.PingRequest{
PaymentId: paymentID,
}
resp, err := a.svc.Ping(ctx, req)
if err != nil {
return nil, err
}
event := resp.GetEvent()
return &client.RoundFinalizationEvent{
ID: event.GetId(),
Tx: event.GetPoolTx(),
ForfeitTxs: event.GetForfeitTxs(),
Tree: treeFromProto{event.GetCongestionTree()}.parse(),
Connectors: event.GetConnectors(),
}, nil
}
func (a *grpcClient) FinalizePayment(
ctx context.Context, req *arkv1.FinalizePaymentRequest,
) (*arkv1.FinalizePaymentResponse, error) {
return a.svc.FinalizePayment(ctx, req)
ctx context.Context, signedForfeitTxs []string,
) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
_, err := a.svc.FinalizePayment(ctx, req)
return err
}
func (a *grpcClient) GetRoundByID(
ctx context.Context, roundID string,
) (*arkv1.GetRoundByIdResponse, error) {
return a.svc.GetRoundById(ctx, &arkv1.GetRoundByIdRequest{
Id: roundID,
})
) (*client.Round, error) {
req := &arkv1.GetRoundByIdRequest{Id: roundID}
resp, err := a.svc.GetRoundById(ctx, req)
if err != nil {
return nil, err
}
round := resp.GetRound()
startedAt := time.Unix(round.GetStart(), 0)
var endedAt *time.Time
if round.GetEnd() > 0 {
t := time.Unix(round.GetEnd(), 0)
endedAt = &t
}
tree := treeFromProto{round.GetCongestionTree()}.parse()
return &client.Round{
ID: round.GetId(),
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: round.GetPoolTx(),
Tree: tree,
ForfeitTxs: round.GetForfeitTxs(),
Connectors: round.GetConnectors(),
Stage: client.RoundStage(int(round.GetStage())),
}, nil
}
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
type out client.Output
for _, level := range treeFromProto.Levels {
func (o out) toProto() *arkv1.Output {
return &arkv1.Output{
Address: o.Address,
Amount: o.Amount,
}
}
type outs []client.Output
func (o outs) toProto() []*arkv1.Output {
list := make([]*arkv1.Output, 0, len(o))
for _, oo := range o {
list = append(list, out(oo).toProto())
}
return list
}
type event struct {
*arkv1.GetEventStreamResponse
}
func (e event) toRoundEvent() client.RoundEvent {
if ee := e.GetRoundFailed(); ee != nil {
return client.RoundFailedEvent{
ID: ee.GetId(),
Reason: ee.GetReason(),
}
}
if ee := e.GetRoundFinalization(); ee != nil {
tree := treeFromProto{ee.GetCongestionTree()}.parse()
return client.RoundFinalizationEvent{
ID: ee.GetId(),
Tx: ee.GetPoolTx(),
ForfeitTxs: ee.GetForfeitTxs(),
Tree: tree,
Connectors: ee.GetConnectors(),
}
}
ee := e.GetRoundFinalized()
return client.RoundFinalizedEvent{
ID: ee.GetId(),
Txid: ee.GetPoolTxid(),
}
}
type vtxo struct {
*arkv1.Vtxo
}
func (v vtxo) toVtxo() client.Vtxo {
var expiresAt *time.Time
if v.GetExpireAt() > 0 {
t := time.Unix(v.GetExpireAt(), 0)
expiresAt = &t
}
return client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(),
},
Amount: v.GetReceiver().GetAmount(),
RoundTxid: v.GetPoolTxid(),
ExpiresAt: expiresAt,
}
}
type vtxos []*arkv1.Vtxo
func (v vtxos) toVtxos() []client.Vtxo {
list := make([]client.Vtxo, 0, len(v))
for _, vv := range v {
list = append(list, vtxo{vv}.toVtxo())
}
return list
}
type input client.VtxoKey
func (i input) toProto() *arkv1.Input {
return &arkv1.Input{
Txid: i.Txid,
Vout: i.VOut,
}
}
type ins []client.VtxoKey
func (i ins) toProto() []*arkv1.Input {
list := make([]*arkv1.Input, 0, len(i))
for _, ii := range i {
list = append(list, input(ii).toProto())
}
return list
}
type treeFromProto struct {
*arkv1.Tree
}
func (t treeFromProto) parse() tree.CongestionTree {
levels := make(tree.CongestionTree, 0, len(t.GetLevels()))
for _, level := range t.GetLevels() {
nodes := make([]tree.Node, 0, len(level.Nodes))
for _, node := range level.Nodes {
@@ -258,7 +339,6 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: false,
})
}
@@ -268,10 +348,39 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
for j, treeLvl := range levels {
for i, node := range treeLvl {
if len(levels.Children(node.Txid)) == 0 {
levels[j][i].Leaf = true
levels[j][i] = tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: true,
}
}
}
}
return levels, nil
return levels
}
type treeToProto tree.CongestionTree
func (t treeToProto) parse() *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(t))
for _, level := range t {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}

View File

@@ -5,15 +5,17 @@ import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/client/rest/service/arkservice"
"github.com/ark-network/ark-sdk/client/rest/service/arkservice/ark_service"
"github.com/ark-network/ark-sdk/client/rest/service/models"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcutil/psbt"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/vulpemventures/go-elements/psetv2"
@@ -23,6 +25,7 @@ type restClient struct {
svc ark_service.ClientService
eventsCh chan client.RoundEventChannel
requestTimeout time.Duration
treeCache *utils.Cache[tree.CongestionTree]
}
func NewClient(aspUrl string) (client.ASPClient, error) {
@@ -35,14 +38,15 @@ func NewClient(aspUrl string) (client.ASPClient, error) {
}
eventsCh := make(chan client.RoundEventChannel)
reqTimeout := 15 * time.Second
treeCache := utils.NewCache[tree.CongestionTree]()
return &restClient{svc, eventsCh, reqTimeout}, nil
return &restClient{svc, eventsCh, reqTimeout, treeCache}, nil
}
func (c *restClient) Close() {}
func (a *restClient) GetEventStream(
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
ctx context.Context, paymentID string,
) (<-chan client.RoundEventChannel, error) {
go func(payID string) {
defer close(a.eventsCh)
@@ -57,9 +61,7 @@ func (a *restClient) GetEventStream(
}
return
default:
resp, err := a.Ping(ctx, &arkv1.PingRequest{
PaymentId: payID,
})
event, err := a.Ping(ctx, payID)
if err != nil {
a.eventsCh <- client.RoundEventChannel{
Err: err,
@@ -67,39 +69,13 @@ func (a *restClient) GetEventStream(
return
}
if resp.GetEvent() != nil {
levels := make([]*arkv1.TreeLevel, 0, len(resp.GetEvent().GetCongestionTree().GetLevels()))
for _, l := range resp.GetEvent().GetCongestionTree().GetLevels() {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
}
if event != nil {
a.eventsCh <- client.RoundEventChannel{
Event: &arkv1.GetEventStreamResponse{
Event: &arkv1.GetEventStreamResponse_RoundFinalization{
RoundFinalization: &arkv1.RoundFinalizationEvent{
Id: resp.GetEvent().GetId(),
PoolTx: resp.GetEvent().GetPoolTx(),
ForfeitTxs: resp.GetEvent().GetForfeitTxs(),
CongestionTree: &arkv1.Tree{
Levels: levels,
},
Connectors: resp.GetEvent().GetConnectors(),
},
},
},
Event: *event,
}
for {
roundID := resp.GetEvent().GetId()
roundID := event.ID
round, err := a.GetRoundByID(ctx, roundID)
if err != nil {
a.eventsCh <- client.RoundEventChannel{
@@ -108,29 +84,20 @@ func (a *restClient) GetEventStream(
return
}
if round.GetRound().GetStage() == arkv1.RoundStage_ROUND_STAGE_FINALIZED {
ptx, _ := psetv2.NewPsetFromBase64(round.GetRound().GetPoolTx())
utx, _ := ptx.UnsignedTx()
if round.Stage == client.RoundStageFinalized {
a.eventsCh <- client.RoundEventChannel{
Event: &arkv1.GetEventStreamResponse{
Event: &arkv1.GetEventStreamResponse_RoundFinalized{
RoundFinalized: &arkv1.RoundFinalizedEvent{
PoolTxid: utx.TxHash().String(),
},
},
Event: client.RoundFinalizedEvent{
ID: roundID,
Txid: getTxid(round.Tx),
},
}
return
}
if round.GetRound().GetStage() == arkv1.RoundStage_ROUND_STAGE_FAILED {
if round.Stage == client.RoundStageFailed {
a.eventsCh <- client.RoundEventChannel{
Event: &arkv1.GetEventStreamResponse{
Event: &arkv1.GetEventStreamResponse_RoundFailed{
RoundFailed: &arkv1.RoundFailed{
Id: round.GetRound().GetId(),
},
},
Event: client.RoundFailedEvent{
ID: roundID,
},
}
return
@@ -150,7 +117,7 @@ func (a *restClient) GetEventStream(
func (a *restClient) GetInfo(
ctx context.Context,
) (*arkv1.GetInfoResponse, error) {
) (*client.Info, error) {
resp, err := a.svc.ArkServiceGetInfo(ark_service.NewArkServiceGetInfoParams())
if err != nil {
return nil, err
@@ -176,7 +143,7 @@ func (a *restClient) GetInfo(
return nil, err
}
return &arkv1.GetInfoResponse{
return &client.Info{
Pubkey: resp.Payload.Pubkey,
RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay),
@@ -188,51 +155,76 @@ func (a *restClient) GetInfo(
func (a *restClient) ListVtxos(
ctx context.Context, addr string,
) (*arkv1.ListVtxosResponse, error) {
) ([]client.Vtxo, []client.Vtxo, error) {
resp, err := a.svc.ArkServiceListVtxos(
ark_service.NewArkServiceListVtxosParams().WithAddress(addr),
)
if err != nil {
return nil, err
return nil, nil, err
}
vtxos := make([]*arkv1.Vtxo, 0, len(resp.Payload.SpendableVtxos))
spendableVtxos := make([]client.Vtxo, 0, len(resp.Payload.SpendableVtxos))
for _, v := range resp.Payload.SpendableVtxos {
expAt, err := strconv.Atoi(v.ExpireAt)
if err != nil {
return nil, err
var expiresAt *time.Time
if v.ExpireAt != "" && v.ExpireAt != "0" {
expAt, err := strconv.Atoi(v.ExpireAt)
if err != nil {
return nil, nil, err
}
t := time.Unix(int64(expAt), 0)
expiresAt = &t
}
amount, err := strconv.Atoi(v.Receiver.Amount)
if err != nil {
return nil, err
return nil, nil, err
}
vtxos = append(vtxos, &arkv1.Vtxo{
Outpoint: &arkv1.Input{
spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid,
Vout: uint32(v.Outpoint.Vout),
VOut: uint32(v.Outpoint.Vout),
},
Receiver: &arkv1.Output{
Address: v.Receiver.Address,
Amount: uint64(amount),
},
Spent: v.Spent,
PoolTxid: v.PoolTxid,
SpentBy: v.SpentBy,
ExpireAt: int64(expAt),
Swept: v.Swept,
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt,
})
}
return &arkv1.ListVtxosResponse{
SpendableVtxos: vtxos,
}, nil
spentVtxos := make([]client.Vtxo, 0, len(resp.Payload.SpentVtxos))
for _, v := range resp.Payload.SpentVtxos {
var expiresAt *time.Time
if v.ExpireAt != "" && v.ExpireAt != "0" {
expAt, err := strconv.Atoi(v.ExpireAt)
if err != nil {
return nil, nil, err
}
t := time.Unix(int64(expAt), 0)
expiresAt = &t
}
amount, err := strconv.Atoi(v.Receiver.Amount)
if err != nil {
return nil, nil, err
}
spentVtxos = append(spentVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt,
})
}
return spendableVtxos, spentVtxos, nil
}
func (a *restClient) GetRound(
ctx context.Context, txID string,
) (*arkv1.GetRoundResponse, error) {
) (*client.Round, error) {
resp, err := a.svc.ArkServiceGetRound(
ark_service.NewArkServiceGetRoundParams().WithTxid(txID),
)
@@ -250,304 +242,125 @@ func (a *restClient) GetRound(
return nil, err
}
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Round.CongestionTree.Levels))
for _, l := range resp.Payload.Round.CongestionTree.Levels {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
startedAt := time.Unix(int64(start), 0)
var endedAt *time.Time
if end > 0 {
t := time.Unix(int64(end), 0)
endedAt = &t
}
return &arkv1.GetRoundResponse{
Round: &arkv1.Round{
Id: resp.Payload.Round.ID,
Start: int64(start),
End: int64(end),
PoolTx: resp.Payload.Round.PoolTx,
CongestionTree: &arkv1.Tree{
Levels: levels,
},
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
},
return &client.Round{
ID: resp.Payload.Round.ID,
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: resp.Payload.Round.PoolTx,
Tree: treeFromProto{resp.Payload.Round.CongestionTree}.parse(),
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
Stage: toRoundStage(*resp.Payload.Round.Stage),
}, nil
}
func (a *restClient) GetSpendableVtxos(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) ([]*client.Vtxo, error) {
allVtxos, err := a.ListVtxos(ctx, addr)
if err != nil {
return nil, err
}
vtxos := make([]*client.Vtxo, 0, len(allVtxos.GetSpendableVtxos()))
for _, v := range allVtxos.GetSpendableVtxos() {
var expireAt *time.Time
if v.ExpireAt > 0 {
t := time.Unix(v.ExpireAt, 0)
expireAt = &t
}
if v.Swept {
continue
}
vtxos = append(vtxos, &client.Vtxo{
Amount: v.Receiver.Amount,
Txid: v.Outpoint.Txid,
VOut: v.Outpoint.Vout,
RoundTxid: v.PoolTxid,
ExpiresAt: expireAt,
})
}
if explorerSvc == nil {
return vtxos, nil
}
redeemBranches, err := a.GetRedeemBranches(ctx, vtxos, explorerSvc)
if err != nil {
return nil, err
}
for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpiresAt()
if err != nil {
return nil, err
}
for i, vtxo := range vtxos {
if vtxo.Txid == vtxoTxid {
vtxos[i].ExpiresAt = expiration
break
}
}
}
return vtxos, nil
}
func (a *restClient) GetRedeemBranches(
ctx context.Context, vtxos []*client.Vtxo, explorerSvc explorer.Explorer,
) (map[string]*client.RedeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0)
redeemBranches := make(map[string]*client.RedeemBranch, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.RoundTxid]; !ok {
round, err := a.GetRound(ctx, vtxo.RoundTxid)
if err != nil {
return nil, err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return nil, err
}
congestionTrees[vtxo.RoundTxid] = congestionTree
}
redeemBranch, err := client.NewRedeemBranch(
explorerSvc, congestionTrees[vtxo.RoundTxid], vtxo,
)
if err != nil {
return nil, err
}
redeemBranches[vtxo.Txid] = redeemBranch
}
return redeemBranches, nil
}
func (a *restClient) GetOffchainBalance(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0)
vtxos, err := a.GetSpendableVtxos(ctx, addr, explorerSvc)
if err != nil {
return 0, nil, err
}
var balance uint64
for _, vtxo := range vtxos {
balance += vtxo.Amount
if vtxo.ExpiresAt != nil {
expiration := vtxo.ExpiresAt.Unix()
if _, ok := amountByExpiration[expiration]; !ok {
amountByExpiration[expiration] = 0
}
amountByExpiration[expiration] += vtxo.Amount
}
}
return balance, amountByExpiration, nil
}
func (a *restClient) Onboard(
ctx context.Context, req *arkv1.OnboardRequest,
) (*arkv1.OnboardResponse, error) {
levels := make([]*models.V1TreeLevel, 0, len(req.GetCongestionTree().GetLevels()))
for _, l := range req.GetCongestionTree().GetLevels() {
nodes := make([]*models.V1Node, 0, len(l.GetNodes()))
for _, n := range l.GetNodes() {
nodes = append(nodes, &models.V1Node{
Txid: n.GetTxid(),
Tx: n.GetTx(),
ParentTxid: n.GetParentTxid(),
})
}
levels = append(levels, &models.V1TreeLevel{
Nodes: nodes,
})
}
congestionTree := models.V1Tree{
Levels: levels,
}
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error {
body := models.V1OnboardRequest{
BoardingTx: req.GetBoardingTx(),
CongestionTree: &congestionTree,
UserPubkey: req.GetUserPubkey(),
BoardingTx: tx,
CongestionTree: treeToProto(congestionTree).parse(),
UserPubkey: userPubkey,
}
_, err := a.svc.ArkServiceOnboard(
ark_service.NewArkServiceOnboardParams().WithBody(&body),
)
if err != nil {
return nil, err
}
return &arkv1.OnboardResponse{}, nil
return err
}
func (a *restClient) RegisterPayment(
ctx context.Context, req *arkv1.RegisterPaymentRequest,
) (*arkv1.RegisterPaymentResponse, error) {
inputs := make([]*models.V1Input, 0, len(req.GetInputs()))
for _, i := range req.GetInputs() {
inputs = append(inputs, &models.V1Input{
Txid: i.GetTxid(),
Vout: int64(i.GetVout()),
ctx context.Context, inputs []client.VtxoKey,
) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs {
ins = append(ins, &models.V1Input{
Txid: i.Txid,
Vout: int64(i.VOut),
})
}
body := models.V1RegisterPaymentRequest{
Inputs: inputs,
Inputs: ins,
}
resp, err := a.svc.ArkServiceRegisterPayment(
ark_service.NewArkServiceRegisterPaymentParams().WithBody(&body),
)
if err != nil {
return nil, err
return "", err
}
return &arkv1.RegisterPaymentResponse{
Id: resp.Payload.ID,
}, nil
return resp.Payload.ID, nil
}
func (a *restClient) ClaimPayment(
ctx context.Context, req *arkv1.ClaimPaymentRequest,
) (*arkv1.ClaimPaymentResponse, error) {
outputs := make([]*models.V1Output, 0, len(req.GetOutputs()))
for _, o := range req.GetOutputs() {
outputs = append(outputs, &models.V1Output{
Address: o.GetAddress(),
Amount: strconv.Itoa(int(o.GetAmount())),
ctx context.Context, paymentID string, outputs []client.Output,
) error {
outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs {
outs = append(outs, &models.V1Output{
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
})
}
body := models.V1ClaimPaymentRequest{
ID: req.GetId(),
Outputs: outputs,
ID: paymentID,
Outputs: outs,
}
_, err := a.svc.ArkServiceClaimPayment(
ark_service.NewArkServiceClaimPaymentParams().WithBody(&body),
)
if err != nil {
return nil, err
}
return &arkv1.ClaimPaymentResponse{}, nil
return err
}
func (a *restClient) Ping(
ctx context.Context, req *arkv1.PingRequest,
) (*arkv1.PingResponse, error) {
ctx context.Context, paymentID string,
) (*client.RoundFinalizationEvent, error) {
r := ark_service.NewArkServicePingParams()
r.SetPaymentID(req.GetPaymentId())
r.SetPaymentID(paymentID)
resp, err := a.svc.ArkServicePing(r)
if err != nil {
return nil, err
}
var event *arkv1.RoundFinalizationEvent
if resp.Payload.Event != nil &&
resp.Payload.Event.ID != "" &&
len(resp.Payload.Event.ForfeitTxs) > 0 &&
len(resp.Payload.Event.CongestionTree.Levels) > 0 &&
len(resp.Payload.Event.Connectors) > 0 &&
resp.Payload.Event.PoolTx != "" {
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Event.CongestionTree.Levels))
for _, l := range resp.Payload.Event.CongestionTree.Levels {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
}
event = &arkv1.RoundFinalizationEvent{
Id: resp.Payload.Event.ID,
PoolTx: resp.Payload.Event.PoolTx,
var event *client.RoundFinalizationEvent
if resp.Payload.Event != nil {
event = &client.RoundFinalizationEvent{
ID: resp.Payload.Event.ID,
Tx: resp.Payload.Event.PoolTx,
ForfeitTxs: resp.Payload.Event.ForfeitTxs,
CongestionTree: &arkv1.Tree{
Levels: levels,
},
Tree: treeFromProto{resp.Payload.Event.CongestionTree}.parse(),
Connectors: resp.Payload.Event.Connectors,
}
}
return &arkv1.PingResponse{
ForfeitTxs: resp.Payload.ForfeitTxs,
Event: event,
}, nil
return event, nil
}
func (a *restClient) FinalizePayment(
ctx context.Context, req *arkv1.FinalizePaymentRequest,
) (*arkv1.FinalizePaymentResponse, error) {
ctx context.Context, signedForfeitTxs []string,
) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
body := models.V1FinalizePaymentRequest{
SignedForfeitTxs: req.GetSignedForfeitTxs(),
}
_, err := a.svc.ArkServiceFinalizePayment(
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
)
if err != nil {
return nil, err
}
return &arkv1.FinalizePaymentResponse{}, nil
return err
}
func (a *restClient) GetRoundByID(
ctx context.Context, roundID string,
) (*arkv1.GetRoundByIdResponse, error) {
) (*client.Round, error) {
resp, err := a.svc.ArkServiceGetRoundByID(
ark_service.NewArkServiceGetRoundByIDParams().WithID(roundID),
)
@@ -565,36 +378,22 @@ func (a *restClient) GetRoundByID(
return nil, err
}
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Round.CongestionTree.Levels))
for _, l := range resp.Payload.Round.CongestionTree.Levels {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
startedAt := time.Unix(int64(start), 0)
var endedAt *time.Time
if end > 0 {
t := time.Unix(int64(end), 0)
endedAt = &t
}
stage := stageStrToInt(*resp.Payload.Round.Stage)
return &arkv1.GetRoundByIdResponse{
Round: &arkv1.Round{
Id: resp.Payload.Round.ID,
Start: int64(start),
End: int64(end),
PoolTx: resp.Payload.Round.PoolTx,
CongestionTree: &arkv1.Tree{
Levels: levels,
},
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
Stage: arkv1.RoundStage(stage),
},
return &client.Round{
ID: resp.Payload.Round.ID,
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: resp.Payload.Round.PoolTx,
Tree: treeFromProto{resp.Payload.Round.CongestionTree}.parse(),
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
Stage: toRoundStage(*resp.Payload.Round.Stage),
}, nil
}
@@ -625,48 +424,83 @@ func newRestClient(
return svc.ArkService, nil
}
func stageStrToInt(stage models.V1RoundStage) int {
func toRoundStage(stage models.V1RoundStage) client.RoundStage {
switch stage {
case models.V1RoundStageROUNDSTAGEUNSPECIFIED:
return 0
case models.V1RoundStageROUNDSTAGEREGISTRATION:
return 1
return client.RoundStageRegistration
case models.V1RoundStageROUNDSTAGEFINALIZATION:
return 2
return client.RoundStageFinalization
case models.V1RoundStageROUNDSTAGEFINALIZED:
return 3
return client.RoundStageFinalized
case models.V1RoundStageROUNDSTAGEFAILED:
return 4
return client.RoundStageFailed
default:
return client.RoundStageUndefined
}
return -1
}
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
type treeFromProto struct {
*models.V1Tree
}
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,
func (t treeFromProto) parse() tree.CongestionTree {
congestionTree := make(tree.CongestionTree, 0, len(t.Levels))
for _, l := range t.Levels {
level := make([]tree.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
level = append(level, tree.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, nodes)
congestionTree = append(congestionTree, level)
}
for j, treeLvl := range levels {
for j, treeLvl := range congestionTree {
for i, node := range treeLvl {
if len(levels.Children(node.Txid)) == 0 {
levels[j][i].Leaf = true
if len(congestionTree.Children(node.Txid)) == 0 {
congestionTree[j][i] = tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: true,
}
}
}
}
return levels, nil
return congestionTree
}
type treeToProto tree.CongestionTree
func (t treeToProto) parse() *models.V1Tree {
levels := make([]*models.V1TreeLevel, 0, len(t))
for _, level := range t {
nodes := make([]*models.V1Node, 0, len(level))
for _, n := range level {
nodes = append(nodes, &models.V1Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &models.V1TreeLevel{
Nodes: nodes,
})
}
return &models.V1Tree{
Levels: levels,
}
}
func getTxid(tx string) string {
if ptx, _ := psetv2.NewPsetFromBase64(tx); ptx != nil {
utx, _ := ptx.UnsignedTx()
return utx.TxHash().String()
}
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true)
return ptx.UnsignedTx.TxID()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,296 +0,0 @@
package arksdk
import (
"bytes"
"context"
"fmt"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/internal/utils"
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"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/psetv2"
)
func (a *arkClient) handleRoundStream(
ctx context.Context,
paymentID string, vtxosToSign []*client.Vtxo, receivers []*arkv1.Output,
) (string, error) {
eventsCh, err := a.client.GetEventStream(ctx, paymentID, &arkv1.GetEventStreamRequest{})
if err != nil {
return "", err
}
var pingStop func()
pingReq := &arkv1.PingRequest{
PaymentId: paymentID,
}
for pingStop == nil {
pingStop = a.ping(ctx, pingReq)
}
defer pingStop()
for {
select {
case <-ctx.Done():
return "", ctx.Err()
case notify := <-eventsCh:
if notify.Err != nil {
return "", err
}
event := notify.Event
if e := event.GetRoundFailed(); e != nil {
pingStop()
return "", fmt.Errorf("round failed: %s", e.GetReason())
}
if e := event.GetRoundFinalization(); e != nil {
pingStop()
log.Info("a round finalization started")
signedForfeitTxs, err := a.handleRoundFinalization(
ctx, e, vtxosToSign, receivers,
)
if err != nil {
return "", err
}
if len(signedForfeitTxs) <= 0 {
log.Info("no forfeit txs to sign, waiting for the next round")
continue
}
log.Info("finalizing payment... ")
_, err = a.client.FinalizePayment(ctx, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
})
if err != nil {
return "", err
}
log.Info("done.")
log.Info("waiting for round finalization...")
}
if event.GetRoundFinalized() != nil {
return event.GetRoundFinalized().GetPoolTxid(), nil
}
}
}
}
func (a *arkClient) handleRoundFinalization(
ctx context.Context,
finalization *arkv1.RoundFinalizationEvent,
vtxosToSign []*client.Vtxo,
receivers []*arkv1.Output,
) ([]string, error) {
if err := a.validateCongestionTree(finalization, receivers); err != nil {
return nil, fmt.Errorf("failed to verify congestion tree: %s", err)
}
return a.loopAndSign(
ctx, finalization.GetForfeitTxs(), vtxosToSign, finalization.GetConnectors(),
)
}
func (a *arkClient) validateCongestionTree(
finalization *arkv1.RoundFinalizationEvent,
receivers []*arkv1.Output,
) error {
poolTx := finalization.GetPoolTx()
ptx, err := psetv2.NewPsetFromBase64(poolTx)
if err != nil {
return err
}
congestionTree, err := utils.ToCongestionTree(
finalization.GetCongestionTree(),
)
if err != nil {
return err
}
connectors := finalization.GetConnectors()
if !utils.IsOnchainOnly(receivers) {
if err := tree.ValidateCongestionTree(
congestionTree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
); err != nil {
return err
}
}
if err := common.ValidateConnectors(poolTx, connectors); err != nil {
return err
}
if err := a.validateReceivers(
ptx, receivers, &congestionTree, a.StoreData.AspPubkey,
); err != nil {
return err
}
log.Infoln("congestion tree validated")
return nil
}
func (a *arkClient) validateReceivers(
ptx *psetv2.Pset,
receivers []*arkv1.Output,
congestionTree *tree.CongestionTree,
aspPubkey *secp256k1.PublicKey,
) error {
for _, receiver := range receivers {
isOnChain, onchainScript, userPubkey, err := utils.DecodeReceiverAddress(
receiver.Address,
)
if err != nil {
return err
}
if isOnChain {
if err := a.validateOnChainReceiver(ptx, receiver, onchainScript); err != nil {
return err
}
} else {
if err := a.validateOffChainReceiver(
congestionTree, receiver, userPubkey, aspPubkey,
); err != nil {
return err
}
}
}
return nil
}
func (a *arkClient) validateOnChainReceiver(
ptx *psetv2.Pset,
receiver *arkv1.Output,
onchainScript []byte,
) error {
found := false
for _, output := range ptx.Outputs {
if bytes.Equal(output.Script, onchainScript) {
if output.Value != receiver.Amount {
return fmt.Errorf(
"invalid collaborative exit output amount: got %d, want %d",
output.Value, receiver.Amount,
)
}
found = true
break
}
}
if !found {
return fmt.Errorf("collaborative exit output not found: %s", receiver.Address)
}
return nil
}
func (a *arkClient) validateOffChainReceiver(
congestionTree *tree.CongestionTree,
receiver *arkv1.Output,
userPubkey, aspPubkey *secp256k1.PublicKey,
) error {
found := false
net := a.explorer.GetNetwork()
outputTapKey, _, _, _, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
)
if err != nil {
return err
}
leaves := congestionTree.Leaves()
for _, leaf := range leaves {
tx, err := psetv2.NewPsetFromBase64(leaf.Tx)
if err != nil {
return err
}
for _, output := range tx.Outputs {
if len(output.Script) == 0 {
continue
}
if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) {
if output.Value == receiver.Amount {
found = true
break
}
}
}
if found {
break
}
}
if !found {
return fmt.Errorf("off-chain send output not found: %s", receiver.Address)
}
return nil
}
func (a *arkClient) loopAndSign(
ctx context.Context,
forfeitTxs []string, vtxosToSign []*client.Vtxo, connectors []string,
) ([]string, error) {
signedForfeits := make([]string, 0)
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, _ := psetv2.NewPsetFromBase64(connector)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, forfeitTx := range forfeitTxs {
pset, err := psetv2.NewPsetFromBase64(forfeitTx)
if err != nil {
return nil, err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
for _, coin := range vtxosToSign {
if inputTxid == coin.Txid {
signedPset, err := a.signForfeitTx(ctx, forfeitTx, pset, connectorsTxids)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedPset)
}
}
}
}
return signedForfeits, nil
}
func (a *arkClient) signForfeitTx(
ctx context.Context, txStr string, tx *psetv2.Pset, connectorsTxids []string,
) (string, error) {
connectorTxid := chainhash.Hash(tx.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, id := range connectorsTxids {
if id == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
return a.wallet.SignTransaction(ctx, a.explorer, txStr)
}

View File

@@ -100,10 +100,7 @@ func main() {
amount := uint64(1000)
receivers := []arksdk.Receiver{
{
To: bobOffchainAddr,
Amount: amount,
},
arksdk.NewLiquidReceiver(bobOffchainAddr, amount),
}
fmt.Println("")
@@ -145,7 +142,7 @@ func setupArkClient() (arksdk.ArkClient, error) {
if err != nil {
return nil, fmt.Errorf("failed to setup store: %s", err)
}
client, err := arksdk.New(storeSvc)
client, err := arksdk.NewCovenantClient(storeSvc)
if err != nil {
return nil, fmt.Errorf("failed to setup ark client: %s", err)
}

View File

@@ -2,11 +2,11 @@ module ark/pkg/client-sdk/example
go 1.22.2
replace github.com/ark-network/ark => ./../../../../server
replace github.com/ark-network/ark => ./../../../../../server
replace github.com/ark-network/ark/common => ./../../../../common
replace github.com/ark-network/ark/common => ./../../../../../common
replace github.com/ark-network/ark-sdk => ./../..
replace github.com/ark-network/ark-sdk => ./../../..
require github.com/ark-network/ark-sdk v0.0.0-00010101000000-000000000000

View File

@@ -18,6 +18,7 @@
async function initWallet() {
try {
const chain = "liquid";
const walletType = "singlekey"
const clientType = "rest"
const privateKey = document.getElementById("prvkey").value;
@@ -31,7 +32,7 @@
logMessage("Init error: asp url is required");
return;
}
await init(walletType, clientType, aspUrl, privateKey, password);
await init(walletType, clientType, aspUrl, privateKey, password, chain);
logMessage("wallet initialized and connected to ASP");
await config();
} catch (err) {
@@ -127,6 +128,7 @@
console.log("unlocking...");
await unlock(password);
console.log(amount, password);
console.log("onboarding...");
const txID = await onboard(amount);
logMessage("Onboarded with amount: " + amount + " and txID: " + txID + ", if in regtest mine a block");
} catch (err) {

View File

@@ -18,7 +18,7 @@ func main() {
store, _ := arksdkwasm.NewLocalStorageStore()
if store != nil {
if err := arksdkwasm.New(ctx, store); err != nil {
if err := arksdkwasm.NewCovenantClient(ctx, store); err != nil {
fmt.Println(err)
}
} else {
@@ -27,7 +27,7 @@ func main() {
fmt.Println(err)
return
}
if err := arksdkwasm.New(ctx, storeSvc); err != nil {
if err := arksdkwasm.NewCovenantClient(ctx, storeSvc); err != nil {
fmt.Println(err)
}
}

View File

@@ -0,0 +1,231 @@
package main
import (
"context"
"fmt"
"io"
"os/exec"
"strings"
"sync"
"time"
arksdk "github.com/ark-network/ark-sdk"
inmemorystore "github.com/ark-network/ark-sdk/store/inmemory"
log "github.com/sirupsen/logrus"
)
var (
aspUrl = "localhost:8080"
clientType = arksdk.GrpcClient
password = "password"
walletType = arksdk.SingleKeyWallet
)
func main() {
ctx := context.Background()
log.Info("alice is setting up her ark wallet...")
aliceArkClient, err := setupArkClient()
if err != nil {
log.Fatal(err)
}
if err := aliceArkClient.Unlock(ctx, password); err != nil {
log.Fatal(err)
}
//nolint:all
defer aliceArkClient.Lock(ctx, password)
log.Info("alice is acquiring onchain funds...")
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx)
if err != nil {
log.Fatal(err)
}
if _, err := runCommand("nigiri", "faucet", aliceOnchainAddr); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
onboardAmount := uint64(20000)
log.Infof("alice is onboarding with %d sats offchain...", onboardAmount)
txid, err := aliceArkClient.Onboard(ctx, onboardAmount)
if err != nil {
log.Fatal(err)
}
if err := generateBlock(); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
log.Infof("alice onboarded with tx: %s", txid)
aliceBalance, err := aliceArkClient.Balance(ctx, false)
if err != nil {
log.Fatal(err)
}
log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount)
log.Infof("alice offchain balance: %d", aliceBalance.OffchainBalance.Total)
fmt.Println("")
log.Info("bob is setting up his ark wallet...")
bobArkClient, err := setupArkClient()
if err != nil {
log.Fatal(err)
}
if err := bobArkClient.Unlock(ctx, password); err != nil {
log.Fatal(err)
}
//nolint:all
defer bobArkClient.Lock(ctx, password)
bobOffchainAddr, _, err := bobArkClient.Receive(ctx)
if err != nil {
log.Fatal(err)
}
bobBalance, err := bobArkClient.Balance(ctx, false)
if err != nil {
log.Fatal(err)
}
log.Infof("bob onchain balance: %d", bobBalance.OnchainBalance.SpendableAmount)
log.Infof("bob offchain balance: %d", bobBalance.OffchainBalance.Total)
amount := uint64(1000)
receivers := []arksdk.Receiver{
arksdk.NewBitcoinReceiver(bobOffchainAddr, amount),
}
fmt.Println("")
log.Infof("alice is sending %d sats to bob offchain...", amount)
txid, err = aliceArkClient.SendOffChain(ctx, false, receivers)
if err != nil {
log.Fatal(err)
}
log.Infof("payment completed in round tx: %s", txid)
if err := generateBlock(); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
aliceBalance, err = aliceArkClient.Balance(ctx, false)
if err != nil {
log.Fatal(err)
}
fmt.Println("")
log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount)
log.Infof("alice offchain balance: %d", aliceBalance.OffchainBalance.Total)
bobBalance, err = bobArkClient.Balance(ctx, false)
if err != nil {
log.Fatal(err)
}
log.Infof("bob onchain balance: %d", bobBalance.OnchainBalance.SpendableAmount)
log.Infof("bob offchain balance: %d", bobBalance.OffchainBalance.Total)
}
func setupArkClient() (arksdk.ArkClient, error) {
storeSvc, err := inmemorystore.NewConfigStore()
if err != nil {
return nil, fmt.Errorf("failed to setup store: %s", err)
}
client, err := arksdk.NewCovenantlessClient(storeSvc)
if err != nil {
return nil, fmt.Errorf("failed to setup ark client: %s", err)
}
if err := client.Init(context.Background(), arksdk.InitArgs{
WalletType: walletType,
ClientType: clientType,
AspUrl: aspUrl,
Password: password,
}); err != nil {
return nil, fmt.Errorf("failed to initialize wallet: %s", err)
}
return client, nil
}
func runCommand(name string, arg ...string) (string, error) {
errb := new(strings.Builder)
cmd := newCommand(name, arg...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
if err := cmd.Start(); err != nil {
return "", err
}
output := new(strings.Builder)
errorb := new(strings.Builder)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if _, err := io.Copy(output, stdout); err != nil {
fmt.Fprintf(errb, "error reading stdout: %s", err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(errorb, stderr); err != nil {
fmt.Fprintf(errb, "error reading stderr: %s", err)
}
}()
wg.Wait()
if err := cmd.Wait(); err != nil {
if errMsg := errorb.String(); len(errMsg) > 0 {
return "", fmt.Errorf(errMsg)
}
if outMsg := output.String(); len(outMsg) > 0 {
return "", fmt.Errorf(outMsg)
}
return "", err
}
if errMsg := errb.String(); len(errMsg) > 0 {
return "", fmt.Errorf(errMsg)
}
return strings.Trim(output.String(), "\n"), nil
}
func newCommand(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
return cmd
}
func generateBlock() error {
if _, err := runCommand("nigiri", "rpc", "generatetoaddress", "1", "bcrt1qgqsguk6wax7ynvav4zys5x290xftk49h5agg0l"); err != nil {
return err
}
time.Sleep(6 * time.Second)
return nil
}

View File

@@ -0,0 +1,25 @@
## USAGE
This example demonstrates how to compile ARK Go SDK to WebAssembly and use it in a web page.
1. Create a Go file with the main package, check [main.go](main.go).
2. Copy `wasm_exec.js`:
```bash
cp $(go env GOROOT)/misc/wasm/wasm_exec.js .
```
3. Build the Go code to WebAssembly:
```bash
GOOS=js GOARCH=wasm go build -o main.wasm main.go
```
4. Load the WebAssembly module in a web page, check [index.html](index.html).
5. Serve the files:
```bash
python3 -m http.server 8000
```

View File

@@ -0,0 +1,61 @@
module ark/pkg/client-sdk/example
go 1.22.2
replace github.com/ark-network/ark => ./../../../../../server
replace github.com/ark-network/ark/common => ./../../../../../common
replace github.com/ark-network/ark-sdk => ./../../..
require github.com/ark-network/ark-sdk v0.0.0-00010101000000-000000000000
require (
github.com/ark-network/ark v0.0.0-00010101000000-000000000000 // indirect
github.com/ark-network/ark/common v0.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
github.com/vulpemventures/go-elements v0.5.4 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,206 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
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.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
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.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.4/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.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 h1:UmfOIiWMZcVMOLaN+lxbbLSuoINGS1WmK1TZNI0b4yk=
github.com/btcsuite/btcd/btcutil/psbt v1.1.9/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY=
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.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/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/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.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
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/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
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/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
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.4 h1:l94xoa9aYPPWiOB7Pmi08rKYvdk/n/sQIbLkQfEAASc=
github.com/vulpemventures/go-elements v0.5.4/go.mod h1:Tvhb+rZWv3lxoI5CdK03J3V+e2QVr/7UAnCYILxFSq4=
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=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg=
google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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=

View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ark SDK WASM Example</title>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
function logMessage(message) {
const logArea = document.getElementById("logArea");
logArea.value += message + "\n";
logArea.scrollTop = logArea.scrollHeight;
}
async function initWallet() {
try {
const chain = "bitcoin";
const walletType = "singlekey"
const clientType = "rest"
const privateKey = document.getElementById("prvkey").value;
const password = document.getElementById("i_password").value;
if (!password) {
logMessage("Init error: password is required");
return;
}
const aspUrl = document.getElementById("aspUrl").value;
if (!aspUrl) {
logMessage("Init error: asp url is required");
return;
}
await init(walletType, clientType, aspUrl, privateKey, password, chain);
logMessage("wallet initialized and connected to ASP");
await config();
} catch (err) {
logMessage("Init error: " + err.message);
}
}
async function receiveAddresses() {
try {
const addresses = await receive();
logMessage("Offchain address: " + addresses.offchainAddr);
logMessage("Onchain address: " + addresses.onchainAddr);
logMessage("If in regtest faucet onchain address: " + addresses.onchainAddr);
} catch (err) {
logMessage("Receive error: " + err.message);
}
}
async function getBalance() {
const bal = await balance(false);
logMessage("Onchain balance: " + bal.onchain_balance)
logMessage("Offchain balance: " + bal.offchain_balance)
}
async function send() {
const password = document.getElementById("s_password").value;
if (!password) {
logMessage("Send error: password is required");
return;
}
try {
const address = document.getElementById("sendAddress").value;
if (!address) {
logMessage("Send error: Address is required");
return;
}
const amountStr = document.getElementById("amountToSend").value;
if (!amountStr) {
logMessage("Send error: Amount is required");
return;
}
const amount = parseInt(amountStr, 10);
await unlock(password);
const txID = await sendOffChain(false, [{ To: address, Amount: amount }]);
logMessage("Sent money with tx ID: " + txID);
} catch (err) {
logMessage("Send error: " + err.message);
} finally {
await lock(password);
}
}
async function config() {
try {
const aspUrl = await getAspUrl();
logMessage("ASP URL: " + aspUrl);
const aspPubKeyHex = await getAspPubKeyHex();
logMessage("ASP PubKey: " + aspPubKeyHex);
const walletType = await getWalletType();
logMessage("Wallet Type: " + walletType);
const clientType = await getClientType();
logMessage("Client Type: " + clientType);
const roundLifetime = await getRoundLifetime();
logMessage("Round Lifetime: " + roundLifetime);
const unilateralExitDelay = await getUnilateralExitDelay();
logMessage("Unilateral Exit Delay: " + unilateralExitDelay);
const minRelayFee = await getMinRelayFee();
logMessage("Min Relay Fee: " + minRelayFee);
} catch (err) {
logMessage("Config error: " + err.message);
}
}
async function board() {
const amountStr = document.getElementById("amount").value;
const amount = parseInt(amountStr, 10);
const password = document.getElementById("o_password").value;
if (!password) {
logMessage("Onboard error: password is required");
return;
}
try {
console.log("unlocking...");
await unlock(password);
console.log(amount, password);
console.log("onboarding...");
const txID = await onboard(amount);
logMessage("Onboarded with amount: " + amount + " and txID: " + txID + ", if in regtest mine a block");
} catch (err) {
logMessage("Onboard error: " + err.message);
} finally {
await lock(password);
}
}
</script>
</head>
<body>
<h1>Ark SDK WASM Example</h1>
<div>
<h2>Wallet</h2>
<div>
<button onclick="initWallet()">Init</button>
<input type="text" id="aspUrl" placeholder="http://localhost:8080">
<input type="password" id="i_password" placeholder="password">
<input type="text" id="prvkey" placeholder="Optional: privkey (hex)">
</div>
<div>
<button onclick="receiveAddresses()">Receive</button>
</div>
<div>
<button onclick="getBalance()">Balance</button>
</div>
<div>
<button onclick="board()">Onboard</button>
<input type="text" id="amount" placeholder="Amount">
<input type="password" id="o_password" placeholder="password">
</div>
<div>
<button onclick="send()">Send</button>
<input type="text" id="sendAddress" placeholder="Offchain Address">
<input type="text" id="amountToSend" placeholder="Amount">
<input type="password" id="s_password" placeholder="password">
</div>
<div>
<button onclick="config()">Config</button>
</div>
</div>
<textarea id="logArea" rows="20" cols="80" readonly></textarea>
</body>
</html>

View File

@@ -0,0 +1,34 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"context"
"fmt"
inmemorystore "github.com/ark-network/ark-sdk/store/inmemory"
arksdkwasm "github.com/ark-network/ark-sdk/wasm"
)
func main() {
var (
ctx = context.Background()
)
store, _ := arksdkwasm.NewLocalStorageStore()
if store != nil {
if err := arksdkwasm.NewCovenantlessClient(ctx, store); err != nil {
fmt.Println(err)
}
} else {
storeSvc, err := inmemorystore.NewConfigStore()
if err != nil {
fmt.Println(err)
return
}
if err := arksdkwasm.NewCovenantlessClient(ctx, storeSvc); err != nil {
fmt.Println(err)
}
}
}

View File

@@ -1,6 +1,24 @@
package explorer
import "github.com/vulpemventures/go-elements/network"
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/ark-network/ark-sdk/internal/utils"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
const (
BitcoinExplorer = "bitcoin"
@@ -11,7 +29,7 @@ type Utxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
Asset string `json:"asset"`
Asset string `json:"asset,omitempty"`
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
@@ -29,6 +47,304 @@ type Explorer interface {
GetTxBlockTime(
txid string,
) (confirmed bool, blocktime int64, err error)
GetNetwork() network.Network
// GetNetwork() common.Network
BaseUrl() string
GetFeeRate() (float64, error)
}
type explorerSvc struct {
cache *utils.Cache[string]
baseUrl string
net common.Network
}
func NewExplorer(baseUrl string, net common.Network) Explorer {
return &explorerSvc{
cache: utils.NewCache[string](),
baseUrl: baseUrl,
net: net,
}
}
func (e *explorerSvc) BaseUrl() string {
return e.baseUrl
}
func (e *explorerSvc) GetNetwork() common.Network {
return e.net
}
func (e *explorerSvc) GetFeeRate() (float64, error) {
endpoint, err := url.JoinPath(e.baseUrl, "fee-estimates")
if err != nil {
return 0, err
}
resp, err := http.Get(endpoint)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var response map[string]float64
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return 0, err
}
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("error getting fee rate: %s", resp.Status)
}
if len(response) == 0 {
log.Debug("empty fee-estimates response, default to 2 sat/vbyte")
return 2, nil
}
return response["1"], nil
}
func (e *explorerSvc) GetTxHex(txid string) (string, error) {
if hex, ok := e.cache.Get(txid); ok {
return hex, nil
}
txHex, err := e.getTxHex(txid)
if err != nil {
return "", err
}
e.cache.Set(txid, txHex)
return txHex, nil
}
func (e *explorerSvc) Broadcast(txStr string) (string, error) {
clone := strings.Clone(txStr)
txStr, txid, err := parseLiquidTx(txStr)
if err != nil {
txStr, txid, err = parseBitcoinTx(clone)
if err != nil {
return "", err
}
}
e.cache.Set(txid, txStr)
txid, err = e.broadcast(txStr)
if err != nil {
if strings.Contains(
strings.ToLower(err.Error()), "transaction already in block chain",
) {
return txid, nil
}
return "", err
}
return txid, nil
}
func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) {
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.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 (e *explorerSvc) GetBalance(addr string) (uint64, error) {
payload, err := e.GetUtxos(addr)
if err != nil {
return 0, err
}
balance := uint64(0)
for _, p := range payload {
balance += p.Amount
}
return balance, nil
}
func (e *explorerSvc) GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)
if err != nil {
return
}
lockedBalance = make(map[int64]uint64, 0)
now := time.Now()
for _, utxo := range utxos {
blocktime := now
if utxo.Status.Confirmed {
blocktime = time.Unix(utxo.Status.Blocktime, 0)
}
delay := time.Duration(unilateralExitDelay) * time.Second
availableAt := blocktime.Add(delay)
if availableAt.After(now) {
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
lockedBalance[availableAt.Unix()] = 0
}
lockedBalance[availableAt.Unix()] += utxo.Amount
} else {
spendableBalance += utxo.Amount
}
}
return
}
func (e *explorerSvc) GetTxBlockTime(
txid string,
) (confirmed bool, blocktime int64, err error) {
resp, err := http.Get(fmt.Sprintf("%s/tx/%s", e.baseUrl, txid))
if err != nil {
return false, 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, 0, err
}
if resp.StatusCode != http.StatusOK {
return false, 0, fmt.Errorf(string(body))
}
var tx struct {
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
if err := json.Unmarshal(body, &tx); err != nil {
return false, 0, err
}
if !tx.Status.Confirmed {
return false, -1, nil
}
return true, tx.Status.Blocktime, nil
}
func (e *explorerSvc) getTxHex(txid string) (string, error) {
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", e.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))
}
hex := string(body)
e.cache.Set(txid, hex)
return hex, nil
}
func (e *explorerSvc) broadcast(txHex string) (string, error) {
body := bytes.NewBuffer([]byte(txHex))
resp, err := http.Post(fmt.Sprintf("%s/tx", e.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 parseLiquidTx(txStr string) (string, string, error) {
tx, err := transaction.NewTxFromHex(txStr)
if err != nil {
pset, err := psetv2.NewPsetFromBase64(txStr)
if err != nil {
return "", "", err
}
tx, err = psetv2.Extract(pset)
if err != nil {
return "", "", err
}
txhex, err := tx.ToHex()
if err != nil {
return "", "", err
}
txid := tx.TxHash().String()
return txhex, txid, nil
}
txhex, err := tx.ToHex()
if err != nil {
return "", "", err
}
txid := tx.TxHash().String()
return txhex, txid, nil
}
func parseBitcoinTx(txStr string) (string, string, error) {
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txStr))); err != nil {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(txStr), true)
if err != nil {
return "", "", err
}
txFromPartial, err := psbt.Extract(ptx)
if err != nil {
return "", "", err
}
tx = *txFromPartial
}
var txBuf bytes.Buffer
if err := tx.Serialize(&txBuf); err != nil {
return "", "", err
}
txhex := hex.EncodeToString(txBuf.Bytes())
txid := tx.TxHash().String()
return txhex, txid, nil
}

View File

@@ -1,241 +0,0 @@
package liquidexplorer
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark/common"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
type liquidExplorer struct {
cache map[string]string
baseUrl string
net string
}
func NewExplorer(baseUrl string, net string) explorer.Explorer {
return &liquidExplorer{
cache: make(map[string]string),
baseUrl: baseUrl,
net: net,
}
}
func (e *liquidExplorer) BaseUrl() string {
return e.baseUrl
}
func (e *liquidExplorer) GetNetwork() network.Network {
return e.liquidNetwork()
}
func (e *liquidExplorer) GetTxHex(txid string) (string, error) {
if hex, ok := e.cache[txid]; ok {
return hex, nil
}
txHex, err := e.getTxHex(txid)
if err != nil {
return "", err
}
e.cache[txid] = txHex
return txHex, nil
}
func (e *liquidExplorer) Broadcast(txStr string) (string, error) {
tx, err := transaction.NewTxFromHex(txStr)
if err != nil {
pset, err := psetv2.NewPsetFromBase64(txStr)
if err != nil {
return "", err
}
tx, err = psetv2.Extract(pset)
if err != nil {
return "", err
}
txStr, _ = tx.ToHex()
}
txid := tx.TxHash().String()
e.cache[txid] = txStr
txid, err = e.broadcast(txStr)
if err != nil {
if strings.Contains(
strings.ToLower(err.Error()), "transaction already in block chain",
) {
return txid, nil
}
return "", err
}
return txid, nil
}
func (e *liquidExplorer) GetUtxos(addr string) ([]explorer.Utxo, error) {
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.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 := []explorer.Utxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return payload, nil
}
func (e *liquidExplorer) GetBalance(addr string) (uint64, error) {
payload, err := e.GetUtxos(addr)
if err != nil {
return 0, err
}
asset := e.liquidNetwork().AssetID
balance := uint64(0)
for _, p := range payload {
if p.Asset != asset {
continue
}
balance += p.Amount
}
return balance, nil
}
func (e *liquidExplorer) GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)
if err != nil {
return
}
lockedBalance = make(map[int64]uint64, 0)
now := time.Now()
for _, utxo := range utxos {
blocktime := now
if utxo.Status.Confirmed {
blocktime = time.Unix(utxo.Status.Blocktime, 0)
}
delay := time.Duration(unilateralExitDelay) * time.Second
availableAt := blocktime.Add(delay)
if availableAt.After(now) {
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
lockedBalance[availableAt.Unix()] = 0
}
lockedBalance[availableAt.Unix()] += utxo.Amount
} else {
spendableBalance += utxo.Amount
}
}
return
}
func (e *liquidExplorer) GetTxBlockTime(
txid string,
) (confirmed bool, blocktime int64, err error) {
resp, err := http.Get(fmt.Sprintf("%s/tx/%s", e.baseUrl, txid))
if err != nil {
return false, 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, 0, err
}
if resp.StatusCode != http.StatusOK {
return false, 0, fmt.Errorf(string(body))
}
var tx struct {
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
if err := json.Unmarshal(body, &tx); err != nil {
return false, 0, err
}
if !tx.Status.Confirmed {
return false, -1, nil
}
return true, tx.Status.Blocktime, nil
}
func (e *liquidExplorer) getTxHex(txid string) (string, error) {
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", e.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))
}
hex := string(body)
e.cache[txid] = hex
return hex, nil
}
func (e *liquidExplorer) broadcast(txHex string) (string, error) {
body := bytes.NewBuffer([]byte(txHex))
resp, err := http.Post(fmt.Sprintf("%s/tx", e.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 (e *liquidExplorer) liquidNetwork() network.Network {
if e.net == common.LiquidTestNet.Name {
return network.Testnet
}
if e.net == common.LiquidRegTest.Name {
return network.Regtest
}
return network.Liquid
}

View File

@@ -1,9 +1,10 @@
package client
package redemption
import (
"fmt"
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -12,8 +13,8 @@ import (
"github.com/vulpemventures/go-elements/taproot"
)
type RedeemBranch struct {
vtxo *Vtxo
type CovenantRedeemBranch struct {
vtxo client.Vtxo
branch []*psetv2.Pset
internalKey *secp256k1.PublicKey
sweepClosure *taproot.TapElementsLeaf
@@ -21,11 +22,11 @@ type RedeemBranch struct {
explorer explorer.Explorer
}
func NewRedeemBranch(
func NewCovenantRedeemBranch(
explorer explorer.Explorer,
congestionTree tree.CongestionTree, vtxo *Vtxo,
) (*RedeemBranch, error) {
sweepClosure, seconds, err := findSweepClosure(congestionTree)
congestionTree tree.CongestionTree, vtxo client.Vtxo,
) (*CovenantRedeemBranch, error) {
sweepClosure, seconds, err := findCovenantSweepClosure(congestionTree)
if err != nil {
return nil, err
}
@@ -55,7 +56,7 @@ func NewRedeemBranch(
return nil, err
}
return &RedeemBranch{
return &CovenantRedeemBranch{
vtxo: vtxo,
branch: branch,
internalKey: internalKey,
@@ -66,10 +67,10 @@ func NewRedeemBranch(
}
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
func (r *RedeemBranch) RedeemPath() ([]string, error) {
func (r *CovenantRedeemBranch) RedeemPath() ([]string, error) {
transactions := make([]string, 0, len(r.branch))
offchainPath, err := r.OffchainPath()
offchainPath, err := r.offchainPath()
if err != nil {
return nil, err
}
@@ -116,7 +117,7 @@ func (r *RedeemBranch) RedeemPath() ([]string, error) {
return transactions, nil
}
func (r *RedeemBranch) ExpiresAt() (*time.Time, error) {
func (r *CovenantRedeemBranch) ExpiresAt() (*time.Time, error) {
lastKnownBlocktime := int64(0)
confirmed, blocktime, _ := r.explorer.GetTxBlockTime(r.vtxo.RoundTxid)
@@ -150,7 +151,7 @@ func (r *RedeemBranch) ExpiresAt() (*time.Time, error) {
}
// offchainPath checks for transactions of the branch onchain and returns only the offchain part
func (r *RedeemBranch) OffchainPath() ([]*psetv2.Pset, error) {
func (r *CovenantRedeemBranch) offchainPath() ([]*psetv2.Pset, error) {
offchainPath := append([]*psetv2.Pset{}, r.branch...)
for i := len(r.branch) - 1; i >= 0; i-- {
@@ -180,7 +181,7 @@ func (r *RedeemBranch) OffchainPath() ([]*psetv2.Pset, error) {
return offchainPath, nil
}
func findSweepClosure(
func findCovenantSweepClosure(
congestionTree tree.CongestionTree,
) (*taproot.TapElementsLeaf, uint, error) {
root, err := congestionTree.Root()

View File

@@ -0,0 +1,193 @@
package redemption
import (
"bytes"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
)
type CovenantlessRedeemBranch struct {
vtxo client.Vtxo
branch []*psbt.Packet
lifetime time.Duration
explorer explorer.Explorer
}
func NewCovenantlessRedeemBranch(
explorer explorer.Explorer,
congestionTree tree.CongestionTree, vtxo client.Vtxo,
) (*CovenantlessRedeemBranch, error) {
_, seconds, err := findCovenantlessSweepClosure(congestionTree)
if err != nil {
return nil, err
}
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
if err != nil {
return nil, err
}
nodes, err := congestionTree.Branch(vtxo.Txid)
if err != nil {
return nil, err
}
branch := make([]*psbt.Packet, 0, len(nodes))
for _, node := range nodes {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
if err != nil {
return nil, err
}
branch = append(branch, ptx)
}
return &CovenantlessRedeemBranch{
vtxo: vtxo,
branch: branch,
lifetime: lifetime,
explorer: explorer,
}, nil
}
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
func (r *CovenantlessRedeemBranch) RedeemPath() ([]string, error) {
transactions := make([]string, 0, len(r.branch))
offchainPath, err := r.OffchainPath()
if err != nil {
return nil, err
}
for _, ptx := range offchainPath {
firstInput := ptx.Inputs[0]
if len(firstInput.TaprootKeySpendSig) == 0 {
return nil, fmt.Errorf("missing taproot key spend signature")
}
var witness bytes.Buffer
if err := psbt.WriteTxWitness(&witness, [][]byte{firstInput.TaprootKeySpendSig}); err != nil {
return nil, err
}
ptx.Inputs[0].FinalScriptWitness = witness.Bytes()
extracted, err := psbt.Extract(ptx)
if err != nil {
return nil, err
}
var txBytes bytes.Buffer
if err := extracted.Serialize(&txBytes); err != nil {
return nil, err
}
transactions = append(transactions, hex.EncodeToString(txBytes.Bytes()))
}
return transactions, nil
}
func (r *CovenantlessRedeemBranch) ExpiresAt() (*time.Time, error) {
lastKnownBlocktime := int64(0)
confirmed, blocktime, _ := r.explorer.GetTxBlockTime(r.vtxo.RoundTxid)
if confirmed {
lastKnownBlocktime = blocktime
} else {
expirationFromNow := time.Now().Add(time.Minute).Add(r.lifetime)
return &expirationFromNow, nil
}
for _, ptx := range r.branch {
txid := ptx.UnsignedTx.TxHash().String()
confirmed, blocktime, err := r.explorer.GetTxBlockTime(txid)
if err != nil {
break
}
if confirmed {
lastKnownBlocktime = blocktime
continue
}
break
}
t := time.Unix(lastKnownBlocktime, 0).Add(r.lifetime)
return &t, nil
}
// offchainPath checks for transactions of the branch onchain and returns only the offchain part
func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) {
offchainPath := append([]*psbt.Packet{}, r.branch...)
for i := len(r.branch) - 1; i >= 0; i-- {
ptx := r.branch[i]
txHash := ptx.UnsignedTx.TxHash().String()
if _, err := r.explorer.GetTxHex(txHash); err != nil {
continue
}
// if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch
if i == len(r.branch)-1 {
offchainPath = []*psbt.Packet{}
} else {
offchainPath = r.branch[i+1:]
}
break
}
return offchainPath, nil
}
func findCovenantlessSweepClosure(
congestionTree tree.CongestionTree,
) (*txscript.TapLeaf, uint, error) {
root, err := congestionTree.Root()
if err != nil {
return nil, 0, err
}
// find the sweep closure
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
if err != nil {
return nil, 0, err
}
var seconds uint
var sweepClosure *txscript.TapLeaf
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
closure := &bitcointree.CSVSigClosure{}
valid, err := closure.Decode(tapLeaf.Script)
if err != nil {
continue
}
if valid && closure.Seconds > seconds {
seconds = closure.Seconds
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
sweepClosure = &leaf
}
}
if sweepClosure == nil {
return nil, 0, fmt.Errorf("sweep closure not found")
}
return sweepClosure, seconds, nil
}

View File

@@ -2,6 +2,7 @@ package utils
import (
"strings"
"sync"
"github.com/ark-network/ark-sdk/client"
)
@@ -22,3 +23,30 @@ func (t SupportedType[V]) Supports(typeStr string) bool {
}
type ClientFactory func(string) (client.ASPClient, error)
type Cache[V any] struct {
mapping map[string]V
lock *sync.RWMutex
}
func NewCache[V any]() *Cache[V] {
return &Cache[V]{
mapping: make(map[string]V),
lock: &sync.RWMutex{},
}
}
func (c Cache[V]) Set(key string, value V) {
c.lock.Lock()
defer c.lock.Unlock()
c.mapping[key] = value
}
func (c Cache[V]) Get(key string) (V, bool) {
c.lock.RLock()
defer c.lock.RUnlock()
val, ok := c.mapping[key]
return val, ok
}

View File

@@ -1,82 +1,29 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"runtime/debug"
"sort"
"strings"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/explorer"
liquidexplorer "github.com/ark-network/ark-sdk/explorer/liquid"
"github.com/ark-network/ark-sdk/store"
"github.com/ark-network/ark-sdk/wallet"
liquidwallet "github.com/ark-network/ark-sdk/wallet/singlekey/liquid"
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
filestore "github.com/ark-network/ark-sdk/wallet/singlekey/store/file"
inmemorystore "github.com/ark-network/ark-sdk/wallet/singlekey/store/inmemory"
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"
"github.com/btcsuite/btcd/chaincfg"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network"
"golang.org/x/crypto/scrypt"
)
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 CastCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}
func CoinSelect(
vtxos []*client.Vtxo, amount, dust uint64, sortByExpirationTime bool,
) ([]*client.Vtxo, uint64, error) {
selected := make([]*client.Vtxo, 0)
notSelected := make([]*client.Vtxo, 0)
vtxos []client.Vtxo, amount, dust uint64, sortByExpirationTime bool,
) ([]client.Vtxo, uint64, error) {
selected := make([]client.Vtxo, 0)
notSelected := make([]client.Vtxo, 0)
selectedAmount := uint64(0)
if sortByExpirationTime {
@@ -131,7 +78,7 @@ func DecodeReceiverAddress(addr string) (
return true, outputScript, nil, nil
}
func IsOnchainOnly(receivers []*arkv1.Output) bool {
func IsOnchainOnly(receivers []client.Output) bool {
for _, receiver := range receivers {
isOnChain, _, _, err := DecodeReceiverAddress(receiver.Address)
if err != nil {
@@ -146,66 +93,6 @@ func IsOnchainOnly(receivers []*arkv1.Output) bool {
return true
}
func GetClient(
supportedClients SupportedType[ClientFactory], clientType, aspUrl string,
) (client.ASPClient, error) {
factory := supportedClients[clientType]
return factory(aspUrl)
}
func GetExplorer(
supportedNetworks SupportedType[string], network string,
) (explorer.Explorer, error) {
url, ok := supportedNetworks[network]
if !ok {
return nil, fmt.Errorf("invalid network")
}
if strings.Contains(network, "liquid") {
return liquidexplorer.NewExplorer(url, network), nil
}
// TODO: support bitcoin explorer
return nil, fmt.Errorf("network not supported yet")
}
func GetWallet(
storeSvc store.ConfigStore, data *store.StoreData, supportedWallets SupportedType[struct{}],
) (wallet.WalletService, error) {
switch data.WalletType {
case wallet.SingleKeyWallet:
return getSingleKeyWallet(storeSvc, data.Network.Name)
default:
return nil, fmt.Errorf(
"unsuported wallet type '%s', please select one of: %s",
data.WalletType, supportedWallets,
)
}
}
func getSingleKeyWallet(
configStore store.ConfigStore, network string,
) (wallet.WalletService, error) {
walletStore, err := getWalletStore(configStore.GetType(), configStore.GetDatadir())
if err != nil {
return nil, err
}
if strings.Contains(network, "liquid") {
return liquidwallet.NewWalletService(configStore, walletStore)
}
// TODO: Support bitcoin wallet
return nil, fmt.Errorf("network %s not supported yet", network)
}
func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error) {
switch storeType {
case store.InMemoryStore:
return inmemorystore.NewWalletStore()
case store.FileStore:
return filestore.NewWalletStore(datadir)
default:
return nil, fmt.Errorf("unknown wallet store type")
}
}
func NetworkFromString(net string) common.Network {
switch net {
case common.Liquid.Name:
@@ -224,3 +111,131 @@ func NetworkFromString(net string) common.Network {
return common.Bitcoin
}
}
func ToElementsNetwork(net common.Network) network.Network {
switch net.Name {
case common.Liquid.Name:
return network.Liquid
case common.LiquidTestNet.Name:
return network.Testnet
case common.LiquidRegTest.Name:
return network.Regtest
default:
return network.Liquid
}
}
func ToBitcoinNetwork(net common.Network) chaincfg.Params {
switch net.Name {
case common.Bitcoin.Name:
return chaincfg.MainNetParams
case common.BitcoinTestNet.Name:
return chaincfg.TestNet3Params
case common.BitcoinRegTest.Name:
return chaincfg.RegressionNetParams
default:
return chaincfg.MainNetParams
}
}
func GenerateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
privKey, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
return privKey, nil
}
func HashPassword(password []byte) []byte {
hash := sha256.Sum256(password)
return hash[:]
}
func EncryptAES128(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 DecryptAES128(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
}

View File

@@ -0,0 +1,293 @@
package singlekeywallet
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
"github.com/ark-network/ark-sdk/store"
"github.com/ark-network/ark-sdk/wallet"
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
)
type bitcoinWallet struct {
*singlekeyWallet
}
func NewBitcoinWallet(
configStore store.ConfigStore, walletStore walletstore.WalletStore,
) (wallet.WalletService, error) {
walletData, err := walletStore.GetWallet()
if err != nil {
return nil, err
}
return &bitcoinWallet{
&singlekeyWallet{configStore, walletStore, nil, walletData},
}, nil
}
func (w *bitcoinWallet) GetAddresses(
ctx context.Context,
) ([]string, []string, []string, error) {
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []string{offchainAddr}
onchainAddrs := []string{onchainAddr}
redemptionAddrs := []string{redemptionAddr}
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
}
func (w *bitcoinWallet) NewAddress(
ctx context.Context, _ bool,
) (string, string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, onchainAddr, nil
}
func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]string, 0, num)
onchainAddrs := make([]string, 0, num)
for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr)
onchainAddrs = append(onchainAddrs, onchainAddr)
}
return offchainAddrs, onchainAddrs, nil
}
func (s *bitcoinWallet) SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (string, error) {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil {
return "", err
}
updater, err := psbt.NewUpdater(ptx)
if err != nil {
return "", err
}
data, err := s.configStore.GetData(ctx)
if err != nil {
return "", err
}
for i, input := range updater.Upsbt.UnsignedTx.TxIn {
if updater.Upsbt.Inputs[i].WitnessUtxo != nil {
continue
}
prevoutTxHex, err := explorerSvc.GetTxHex(input.PreviousOutPoint.Hash.String())
if err != nil {
return "", err
}
var prevoutTx wire.MsgTx
if err := prevoutTx.Deserialize(hex.NewDecoder(strings.NewReader(prevoutTxHex))); err != nil {
return "", err
}
utxo := prevoutTx.TxOut[input.PreviousOutPoint.Index]
if utxo == nil {
return "", fmt.Errorf("witness utxo not found")
}
if err := updater.AddInWitnessUtxo(utxo, i); err != nil {
return "", err
}
sighashType := txscript.SigHashAll
if utxo.PkScript[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(sighashType, i); err != nil {
return "", err
}
}
_, onchainAddr, _, err := s.getAddress(ctx)
if err != nil {
return "", err
}
net := utils.ToBitcoinNetwork(data.Network)
addr, _ := btcutil.DecodeAddress(onchainAddr, &net)
onchainWalletScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return "", err
}
prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range updater.Upsbt.Inputs {
outpoint := updater.Upsbt.UnsignedTx.TxIn[i].PreviousOutPoint
prevouts[outpoint] = input.WitnessUtxo
}
prevoutFetcher := txscript.NewMultiPrevOutFetcher(
prevouts,
)
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
for i, input := range ptx.Inputs {
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
return "", err
}
preimage, err := txscript.CalcWitnessSigHash(
input.WitnessUtxo.PkScript,
txsighashes,
txscript.SigHashAll,
updater.Upsbt.UnsignedTx,
i,
int64(input.WitnessUtxo.Value),
)
if err != nil {
return "", err
}
sig := ecdsa.Sign(s.privateKey, preimage)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
{
PubKey: s.walletData.Pubkey.SerializeCompressed(),
Signature: signatureWithSighashType,
},
}
continue
}
if len(input.TaprootLeafScript) > 0 {
pubkey := s.walletData.Pubkey
for _, leaf := range input.TaprootLeafScript {
closure, err := bitcointree.DecodeClosure(leaf.Script)
if err != nil {
return "", err
}
sign := false
switch c := closure.(type) {
case *bitcointree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
case *bitcointree.ForfeitClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
}
if sign {
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
return "", err
}
hash := txscript.NewTapLeaf(leaf.LeafVersion, leaf.Script).TapHash()
preimage, err := txscript.CalcTapscriptSignaturehash(
txsighashes,
txscript.SigHashDefault,
ptx.UnsignedTx,
i,
prevoutFetcher,
txscript.NewBaseTapLeaf(leaf.Script),
)
if err != nil {
return "", err
}
sig, err := schnorr.Sign(s.privateKey, preimage)
if err != nil {
return "", err
}
if !sig.Verify(preimage, pubkey) {
return "", fmt.Errorf("signature verification failed")
}
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{
{
XOnlyPubKey: schnorr.SerializePubKey(pubkey),
LeafHash: hash.CloneBytes(),
Signature: sig.Serialize(),
SigHash: txscript.SigHashDefault,
},
}
}
}
}
}
return ptx.B64Encode()
}
func (w *bitcoinWallet) getAddress(
ctx context.Context,
) (string, string, string, error) {
if w.walletData == nil {
return "", "", "", fmt.Errorf("wallet not initialized")
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return "", "", "", err
}
offchainAddr, err := common.EncodeAddress(data.Network.Addr, w.walletData.Pubkey, data.AspPubkey)
if err != nil {
return "", "", "", err
}
netParams := utils.ToBitcoinNetwork(data.Network)
onchainAddr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(w.walletData.Pubkey.SerializeCompressed()), &netParams)
if err != nil {
return "", "", "", err
}
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
)
if err != nil {
return "", "", "", err
}
redemptionAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&netParams,
)
if err != nil {
return "", "", "", err
}
return offchainAddr, onchainAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil
}

View File

@@ -1,19 +0,0 @@
package liquidwallet
import (
"github.com/ark-network/ark/common"
"github.com/vulpemventures/go-elements/network"
)
func toElementsNetwork(net common.Network) network.Network {
switch net.Name {
case common.Liquid.Name:
return network.Liquid
case common.LiquidTestNet.Name:
return network.Testnet
case common.LiquidRegTest.Name:
return network.Regtest
default:
return network.Liquid
}
}

View File

@@ -1,143 +1,43 @@
package liquidwallet
package singlekeywallet
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
"github.com/ark-network/ark-sdk/store"
"github.com/ark-network/ark-sdk/wallet"
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
"github.com/ark-network/ark-sdk/wallet/singlekey/utils"
"github.com/ark-network/ark/common"
"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/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
type singlekeyWallet struct {
configStore store.ConfigStore
walletStore walletstore.WalletStore
privateKey *secp256k1.PrivateKey
walletData *walletstore.WalletData
type liquidWallet struct {
*singlekeyWallet
}
func NewWalletService(
func NewLiquidWallet(
configStore store.ConfigStore, walletStore walletstore.WalletStore,
) (wallet.WalletService, error) {
walletData, err := walletStore.GetWallet()
if err != nil {
return nil, err
}
return &singlekeyWallet{configStore, walletStore, nil, walletData}, nil
return &liquidWallet{
&singlekeyWallet{configStore, walletStore, nil, walletData},
}, nil
}
func (w *singlekeyWallet) GetType() string {
return wallet.SingleKeyWallet
}
func (w *singlekeyWallet) Create(
_ context.Context, password, seed string,
) (string, error) {
var privateKey *secp256k1.PrivateKey
if len(seed) <= 0 {
privKey, err := utils.GenerateRandomPrivateKey()
if err != nil {
return "", err
}
privateKey = privKey
} else {
privKeyBytes, err := hex.DecodeString(seed)
if err != nil {
return "", err
}
privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes)
}
pwd := []byte(password)
passwordHash := utils.HashPassword(pwd)
pubkey := privateKey.PubKey()
buf := privateKey.Serialize()
encryptedPrivateKey, err := utils.EncryptAES128(buf, pwd)
if err != nil {
return "", err
}
walletData := walletstore.WalletData{
EncryptedPrvkey: encryptedPrivateKey,
PasswordHash: passwordHash,
Pubkey: pubkey,
}
if err := w.walletStore.AddWallet(walletData); err != nil {
return "", err
}
w.walletData = &walletData
return hex.EncodeToString(privateKey.Serialize()), nil
}
func (w *singlekeyWallet) Lock(_ context.Context, password string) error {
if w.walletData == nil {
return fmt.Errorf("wallet not initialized")
}
if w.privateKey == nil {
return nil
}
pwd := []byte(password)
currentPassHash := utils.HashPassword(pwd)
if !bytes.Equal(w.walletData.PasswordHash, currentPassHash) {
return fmt.Errorf("invalid password")
}
w.privateKey = nil
return nil
}
func (w *singlekeyWallet) Unlock(
_ context.Context, password string,
) (bool, error) {
if w.walletData == nil {
return false, fmt.Errorf("wallet not initialized")
}
if w.privateKey != nil {
return true, nil
}
pwd := []byte(password)
currentPassHash := utils.HashPassword(pwd)
if !bytes.Equal(w.walletData.PasswordHash, currentPassHash) {
return false, fmt.Errorf("invalid password")
}
privateKeyBytes, err := utils.DecryptAES128(w.walletData.EncryptedPrvkey, pwd)
if err != nil {
return false, err
}
w.privateKey = secp256k1.PrivKeyFromBytes(privateKeyBytes)
return false, nil
}
func (w *singlekeyWallet) IsLocked() bool {
return w.privateKey == nil
}
func (w *singlekeyWallet) GetAddresses(
func (w *liquidWallet) GetAddresses(
ctx context.Context,
) ([]string, []string, []string, error) {
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
@@ -151,7 +51,7 @@ func (w *singlekeyWallet) GetAddresses(
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
}
func (w *singlekeyWallet) NewAddress(
func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool,
) (string, string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
@@ -161,7 +61,7 @@ func (w *singlekeyWallet) NewAddress(
return offchainAddr, onchainAddr, nil
}
func (w *singlekeyWallet) NewAddresses(
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
@@ -178,7 +78,7 @@ func (w *singlekeyWallet) NewAddresses(
return offchainAddrs, onchainAddrs, nil
}
func (s *singlekeyWallet) SignTransaction(
func (s *liquidWallet) SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (string, error) {
pset, err := psetv2.NewPsetFromBase64(tx)
@@ -234,7 +134,7 @@ func (s *singlekeyWallet) SignTransaction(
if err != nil {
return "", err
}
liquidNet := toElementsNetwork(storeData.Network)
liquidNet := utils.ToElementsNetwork(storeData.Network)
p2wpkh := payment.FromPublicKey(s.walletData.Pubkey, &liquidNet, nil)
onchainWalletScript := p2wpkh.WitnessScript
@@ -355,7 +255,7 @@ func (s *singlekeyWallet) SignTransaction(
return pset.ToBase64()
}
func (w *singlekeyWallet) getAddress(
func (w *liquidWallet) getAddress(
ctx context.Context,
) (string, string, string, error) {
if w.walletData == nil {
@@ -374,7 +274,7 @@ func (w *singlekeyWallet) getAddress(
return "", "", "", err
}
liquidNet := toElementsNetwork(data.Network)
liquidNet := utils.ToElementsNetwork(data.Network)
p2wpkh := payment.FromPublicKey(w.walletData.Pubkey, &liquidNet, nil)
onchainAddr, err := p2wpkh.WitnessPubKeyHash()

View File

@@ -1,116 +0,0 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"runtime/debug"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"golang.org/x/crypto/scrypt"
)
func GenerateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
privKey, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
return privKey, nil
}
func HashPassword(password []byte) []byte {
hash := sha256.Sum256(password)
return hash[:]
}
func EncryptAES128(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 DecryptAES128(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
}

View File

@@ -0,0 +1,118 @@
package singlekeywallet
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"github.com/ark-network/ark-sdk/internal/utils"
"github.com/ark-network/ark-sdk/store"
"github.com/ark-network/ark-sdk/wallet"
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
type singlekeyWallet struct {
configStore store.ConfigStore
walletStore walletstore.WalletStore
privateKey *secp256k1.PrivateKey
walletData *walletstore.WalletData
}
func (w *singlekeyWallet) GetType() string {
return wallet.SingleKeyWallet
}
func (w *singlekeyWallet) Create(
_ context.Context, password, seed string,
) (string, error) {
var privateKey *secp256k1.PrivateKey
if len(seed) <= 0 {
privKey, err := utils.GenerateRandomPrivateKey()
if err != nil {
return "", err
}
privateKey = privKey
} else {
privKeyBytes, err := hex.DecodeString(seed)
if err != nil {
return "", err
}
privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes)
}
pwd := []byte(password)
passwordHash := utils.HashPassword(pwd)
pubkey := privateKey.PubKey()
buf := privateKey.Serialize()
encryptedPrivateKey, err := utils.EncryptAES128(buf, pwd)
if err != nil {
return "", err
}
walletData := walletstore.WalletData{
EncryptedPrvkey: encryptedPrivateKey,
PasswordHash: passwordHash,
Pubkey: pubkey,
}
if err := w.walletStore.AddWallet(walletData); err != nil {
return "", err
}
w.walletData = &walletData
return hex.EncodeToString(privateKey.Serialize()), nil
}
func (w *singlekeyWallet) Lock(_ context.Context, password string) error {
if w.walletData == nil {
return fmt.Errorf("wallet not initialized")
}
if w.privateKey == nil {
return nil
}
pwd := []byte(password)
currentPassHash := utils.HashPassword(pwd)
if !bytes.Equal(w.walletData.PasswordHash, currentPassHash) {
return fmt.Errorf("invalid password")
}
w.privateKey = nil
return nil
}
func (w *singlekeyWallet) Unlock(
_ context.Context, password string,
) (bool, error) {
if w.walletData == nil {
return false, fmt.Errorf("wallet not initialized")
}
if w.privateKey != nil {
return true, nil
}
pwd := []byte(password)
currentPassHash := utils.HashPassword(pwd)
if !bytes.Equal(w.walletData.PasswordHash, currentPassHash) {
return false, fmt.Errorf("invalid password")
}
privateKeyBytes, err := utils.DecryptAES128(w.walletData.EncryptedPrvkey, pwd)
if err != nil {
return false, err
}
w.privateKey = secp256k1.PrivKeyFromBytes(privateKeyBytes)
return false, nil
}
func (w *singlekeyWallet) IsLocked() bool {
return w.privateKey == nil
}

View File

@@ -2,13 +2,14 @@ package wallet_test
import (
"context"
"strings"
"testing"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/store"
inmemorystore "github.com/ark-network/ark-sdk/store/inmemory"
"github.com/ark-network/ark-sdk/wallet"
liquidwallet "github.com/ark-network/ark-sdk/wallet/singlekey/liquid"
singlekeywallet "github.com/ark-network/ark-sdk/wallet/singlekey"
inmemorywalletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store/inmemory"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2"
@@ -35,10 +36,15 @@ func TestWallet(t *testing.T) {
args []interface{}
}{
{
name: wallet.SingleKeyWallet,
name: "liquid" + wallet.SingleKeyWallet,
chain: "liquid",
args: []interface{}{common.LiquidRegTest},
},
{
name: "bitcoin" + wallet.SingleKeyWallet,
chain: "bitcoin",
args: []interface{}{common.LiquidRegTest},
},
}
for i := range tests {
@@ -59,7 +65,9 @@ func TestWallet(t *testing.T) {
var walletSvc wallet.WalletService
if tt.chain == "liquid" {
walletSvc, err = liquidwallet.NewWalletService(store, walletStore)
walletSvc, err = singlekeywallet.NewLiquidWallet(store, walletStore)
} else {
walletSvc, err = singlekeywallet.NewBitcoinWallet(store, walletStore)
}
require.NoError(t, err)
require.NotNil(t, walletSvc)
@@ -85,7 +93,7 @@ func TestWallet(t *testing.T) {
require.NotEmpty(t, onchainAddr)
expectedNumOfAddresses := 2
if tt.name == wallet.SingleKeyWallet {
if strings.Contains(tt.name, wallet.SingleKeyWallet) {
expectedNumOfAddresses = 1
}
@@ -102,7 +110,7 @@ func TestWallet(t *testing.T) {
require.Len(t, onchainAddrs, num)
expectedNumOfAddresses += num
if tt.name == wallet.SingleKeyWallet {
if strings.Contains(tt.name, wallet.SingleKeyWallet) {
expectedNumOfAddresses = 1
}
offchainAddrs, onchainAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx)

View File

@@ -9,7 +9,7 @@ import (
arksdk "github.com/ark-network/ark-sdk"
"github.com/ark-network/ark-sdk/store"
"github.com/ark-network/ark-sdk/wallet"
liquidwallet "github.com/ark-network/ark-sdk/wallet/singlekey/liquid"
singlekeywallet "github.com/ark-network/ark-sdk/wallet/singlekey"
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
)
@@ -41,7 +41,7 @@ func init() {
js.Global().Set("getMinRelayFee", GetMinRelayFeeWrapper())
}
func New(
func NewCovenantClient(
ctx context.Context, storeSvc store.ConfigStore,
) error {
var err error
@@ -52,7 +52,7 @@ func New(
}
if data == nil {
arkSdkClient, err = arksdk.New(storeSvc)
arkSdkClient, err = arksdk.NewCovenantClient(storeSvc)
} else {
var walletSvc wallet.WalletService
switch data.WalletType {
@@ -65,7 +65,42 @@ func New(
default:
return fmt.Errorf("unknown wallet type")
}
arkSdkClient, err = arksdk.LoadWithWallet(storeSvc, walletSvc)
arkSdkClient, err = arksdk.LoadCovenantClientWithWallet(storeSvc, walletSvc)
}
if err != nil {
js.Global().Get("console").Call("error", err.Error())
return err
}
configStore = storeSvc
select {}
}
func NewCovenantlessClient(
ctx context.Context, storeSvc store.ConfigStore,
) error {
var err error
data, err := storeSvc.GetData(ctx)
if err != nil {
return err
}
if data == nil {
arkSdkClient, err = arksdk.NewCovenantlessClient(storeSvc)
} else {
var walletSvc wallet.WalletService
switch data.WalletType {
case arksdk.SingleKeyWallet:
walletSvc, err = getSingleKeyWallet(storeSvc, data.Network.Name)
if err != nil {
return err
}
// TODO: Support HD wallet
default:
return fmt.Errorf("unknown wallet type")
}
arkSdkClient, err = arksdk.LoadCovenantlessClientWithWallet(storeSvc, walletSvc)
}
if err != nil {
js.Global().Get("console").Call("error", err.Error())
@@ -92,8 +127,7 @@ func getSingleKeyWallet(
return nil, err
}
if strings.Contains(network, "liquid") {
return liquidwallet.NewWalletService(configStore, walletStore)
return singlekeywallet.NewLiquidWallet(configStore, walletStore)
}
// TODO: Support bitcoin wallet
return nil, fmt.Errorf("network %s not supported yet", network)
return singlekeywallet.NewBitcoinWallet(configStore, walletStore)
}

View File

@@ -9,7 +9,7 @@ import (
arksdk "github.com/ark-network/ark-sdk"
"github.com/ark-network/ark-sdk/wallet"
liquidwallet "github.com/ark-network/ark-sdk/wallet/singlekey/liquid"
singlekeywallet "github.com/ark-network/ark-sdk/wallet/singlekey"
)
func LogWrapper() js.Func {
@@ -25,9 +25,13 @@ func logMsg(msg string) {
func InitWrapper() js.Func {
return JSPromise(func(args []js.Value) (interface{}, error) {
if len(args) != 5 {
if len(args) != 6 {
return nil, errors.New("invalid number of args")
}
chain := args[5].String()
if chain != "bitcoin" && chain != "liquid" {
return nil, errors.New("invalid chain, select either 'bitcoin' or 'liquid'")
}
var walletSvc wallet.WalletService
switch args[0].String() {
@@ -36,9 +40,16 @@ func InitWrapper() js.Func {
if err != nil {
return nil, fmt.Errorf("failed to init wallet store: %s", err)
}
walletSvc, err = liquidwallet.NewWalletService(configStore, walletStore)
if err != nil {
return nil, err
if chain == "liquid" {
walletSvc, err = singlekeywallet.NewLiquidWallet(configStore, walletStore)
if err != nil {
return nil, err
}
} else {
walletSvc, err = singlekeywallet.NewBitcoinWallet(configStore, walletStore)
if err != nil {
return nil, err
}
}
default:
return nil, fmt.Errorf("unsupported wallet type")
@@ -150,10 +161,9 @@ func SendOnChainWrapper() js.Func {
receivers := make([]arksdk.Receiver, args[0].Length())
for i := 0; i < args[0].Length(); i++ {
receiver := args[0].Index(i)
receivers[i] = arksdk.Receiver{
To: receiver.Get("To").String(),
Amount: uint64(receiver.Get("Amount").Int()),
}
receivers[i] = arksdk.NewLiquidReceiver(
receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()),
)
}
txID, err := arkSdkClient.SendOnChain(
@@ -175,10 +185,9 @@ func SendOffChainWrapper() js.Func {
receivers := make([]arksdk.Receiver, args[1].Length())
for i := 0; i < args[1].Length(); i++ {
receiver := args[1].Index(i)
receivers[i] = arksdk.Receiver{
To: receiver.Get("To").String(),
Amount: uint64(receiver.Get("Amount").Int()),
}
receivers[i] = arksdk.NewLiquidReceiver(
receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()),
)
}
txID, err := arkSdkClient.SendOffChain(

View File

@@ -497,12 +497,10 @@ func (s *covenantlessService) finalizeRound() {
log.Debugf("signing round transaction %s\n", round.Id)
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
if err != nil {
fmt.Println(err)
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
fmt.Println(signedPoolTx)
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
if err != nil {

View File

@@ -149,7 +149,6 @@ func (s *sweeper) createTask(
for _, input := range inputs {
// sweepableVtxos related to the sweep input
sweepableVtxos := make([]domain.VtxoKey, 0)
fmt.Println("input", input.GetHash().String(), input.GetIndex())
// check if input is the vtxo itself
vtxos, _ := s.repoManager.Vtxos().GetVtxos(

View File

@@ -2,7 +2,6 @@ package txbuilder
import (
"context"
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
@@ -34,8 +33,6 @@ func sweepTransaction(
return nil, err
}
fmt.Println("sweepClosure.Pubkey", hex.EncodeToString(sweepClosure.Pubkey.SerializeCompressed()))
if !valid {
return nil, fmt.Errorf("invalid csv script")
}

View File

@@ -93,6 +93,7 @@ func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
}
}
// TODO: @louisinger shall we delete this code?
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet)
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
@@ -110,8 +111,6 @@ func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
// return nil, err
// }
// fmt.Println("PREIMAGE", hex.EncodeToString(preimage))
return s.wallet.SignPsbt(packet)
}