mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 12:44:19 +01:00
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:
committed by
GitHub
parent
1c67c56d9d
commit
8de2df3d7f
@@ -141,6 +141,44 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
|||||||
return valid, nil
|
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) {
|
func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
|
||||||
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
|
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
|
||||||
if data32Index == -1 {
|
if data32Index == -1 {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
282
pkg/client-sdk/client.go
Normal file
282
pkg/client-sdk/client.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/explorer"
|
"github.com/ark-network/ark/common/tree"
|
||||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -13,48 +12,121 @@ const (
|
|||||||
RestClient = "rest"
|
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 {
|
type RoundEventChannel struct {
|
||||||
Event *arkv1.GetEventStreamResponse
|
Event RoundEvent
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Vtxo struct {
|
type VtxoKey struct {
|
||||||
Amount uint64
|
|
||||||
Txid string
|
Txid string
|
||||||
VOut uint32
|
VOut uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vtxo struct {
|
||||||
|
VtxoKey
|
||||||
|
Amount uint64
|
||||||
RoundTxid string
|
RoundTxid string
|
||||||
ExpiresAt *time.Time
|
ExpiresAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type ASPClient interface {
|
type Output struct {
|
||||||
GetInfo(ctx context.Context) (*arkv1.GetInfoResponse, error)
|
Address string
|
||||||
ListVtxos(ctx context.Context, addr string) (*arkv1.ListVtxosResponse, error)
|
Amount uint64
|
||||||
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 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() {}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/client"
|
"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"
|
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -19,6 +19,7 @@ type grpcClient struct {
|
|||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
svc arkv1.ArkServiceClient
|
svc arkv1.ArkServiceClient
|
||||||
eventsCh chan client.RoundEventChannel
|
eventsCh chan client.RoundEventChannel
|
||||||
|
treeCache *utils.Cache[tree.CongestionTree]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(aspUrl string) (client.ASPClient, error) {
|
func NewClient(aspUrl string) (client.ASPClient, error) {
|
||||||
@@ -43,8 +44,9 @@ func NewClient(aspUrl string) (client.ASPClient, error) {
|
|||||||
|
|
||||||
svc := arkv1.NewArkServiceClient(conn)
|
svc := arkv1.NewArkServiceClient(conn)
|
||||||
eventsCh := make(chan client.RoundEventChannel)
|
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() {
|
func (c *grpcClient) Close() {
|
||||||
@@ -53,8 +55,9 @@ func (c *grpcClient) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *grpcClient) GetEventStream(
|
func (a *grpcClient) GetEventStream(
|
||||||
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
|
ctx context.Context, paymentID string,
|
||||||
) (<-chan client.RoundEventChannel, error) {
|
) (<-chan client.RoundEventChannel, error) {
|
||||||
|
req := &arkv1.GetEventStreamRequest{}
|
||||||
stream, err := a.svc.GetEventStream(ctx, req)
|
stream, err := a.svc.GetEventStream(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -70,187 +73,265 @@ func (a *grpcClient) GetEventStream(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.eventsCh <- client.RoundEventChannel{Event: resp}
|
a.eventsCh <- client.RoundEventChannel{Event: event{resp}.toRoundEvent()}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return a.eventsCh, nil
|
return a.eventsCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *grpcClient) GetInfo(ctx context.Context) (*arkv1.GetInfoResponse, error) {
|
func (a *grpcClient) GetInfo(ctx context.Context) (*client.Info, error) {
|
||||||
return a.svc.GetInfo(ctx, &arkv1.GetInfoRequest{})
|
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(
|
func (a *grpcClient) ListVtxos(
|
||||||
ctx context.Context,
|
ctx context.Context, addr string,
|
||||||
addr string,
|
) ([]client.Vtxo, []client.Vtxo, error) {
|
||||||
) (*arkv1.ListVtxosResponse, error) {
|
resp, err := a.svc.ListVtxos(ctx, &arkv1.ListVtxosRequest{Address: addr})
|
||||||
return 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(
|
func (a *grpcClient) GetRound(
|
||||||
ctx context.Context, txID string,
|
ctx context.Context, txID string,
|
||||||
) (*arkv1.GetRoundResponse, error) {
|
) (*client.Round, error) {
|
||||||
return a.svc.GetRound(ctx, &arkv1.GetRoundRequest{Txid: txID})
|
req := &arkv1.GetRoundRequest{Txid: txID}
|
||||||
}
|
resp, err := a.svc.GetRound(ctx, req)
|
||||||
|
|
||||||
func (a *grpcClient) GetSpendableVtxos(
|
|
||||||
ctx context.Context, addr string, explorerSvc explorer.Explorer,
|
|
||||||
) ([]*client.Vtxo, error) {
|
|
||||||
allVtxos, err := a.ListVtxos(ctx, addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
round := resp.GetRound()
|
||||||
vtxos := make([]*client.Vtxo, 0, len(allVtxos.GetSpendableVtxos()))
|
startedAt := time.Unix(round.GetStart(), 0)
|
||||||
for _, v := range allVtxos.GetSpendableVtxos() {
|
var endedAt *time.Time
|
||||||
var expireAt *time.Time
|
if round.GetEnd() > 0 {
|
||||||
if v.ExpireAt > 0 {
|
t := time.Unix(round.GetEnd(), 0)
|
||||||
t := time.Unix(v.ExpireAt, 0)
|
endedAt = &t
|
||||||
expireAt = &t
|
|
||||||
}
|
}
|
||||||
if v.Swept {
|
return &client.Round{
|
||||||
continue
|
ID: round.GetId(),
|
||||||
}
|
StartedAt: &startedAt,
|
||||||
vtxos = append(vtxos, &client.Vtxo{
|
EndedAt: endedAt,
|
||||||
Amount: v.Receiver.Amount,
|
Tx: round.GetPoolTx(),
|
||||||
Txid: v.Outpoint.Txid,
|
Tree: treeFromProto{round.GetCongestionTree()}.parse(),
|
||||||
VOut: v.Outpoint.Vout,
|
ForfeitTxs: round.GetForfeitTxs(),
|
||||||
RoundTxid: v.PoolTxid,
|
Connectors: round.GetConnectors(),
|
||||||
ExpiresAt: expireAt,
|
Stage: client.RoundStage(int(round.GetStage())),
|
||||||
})
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *grpcClient) Onboard(
|
func (a *grpcClient) Onboard(
|
||||||
ctx context.Context, req *arkv1.OnboardRequest,
|
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
|
||||||
) (*arkv1.OnboardResponse, error) {
|
) error {
|
||||||
return a.svc.Onboard(ctx, req)
|
req := &arkv1.OnboardRequest{
|
||||||
|
BoardingTx: tx,
|
||||||
|
UserPubkey: userPubkey,
|
||||||
|
CongestionTree: treeToProto(congestionTree).parse(),
|
||||||
|
}
|
||||||
|
_, err := a.svc.Onboard(ctx, req)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *grpcClient) RegisterPayment(
|
func (a *grpcClient) RegisterPayment(
|
||||||
ctx context.Context, req *arkv1.RegisterPaymentRequest,
|
ctx context.Context, inputs []client.VtxoKey,
|
||||||
) (*arkv1.RegisterPaymentResponse, error) {
|
) (string, error) {
|
||||||
return a.svc.RegisterPayment(ctx, req)
|
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(
|
func (a *grpcClient) ClaimPayment(
|
||||||
ctx context.Context, req *arkv1.ClaimPaymentRequest,
|
ctx context.Context, paymentID string, outputs []client.Output,
|
||||||
) (*arkv1.ClaimPaymentResponse, error) {
|
) error {
|
||||||
return a.svc.ClaimPayment(ctx, req)
|
req := &arkv1.ClaimPaymentRequest{
|
||||||
|
Id: paymentID,
|
||||||
|
Outputs: outs(outputs).toProto(),
|
||||||
|
}
|
||||||
|
_, err := a.svc.ClaimPayment(ctx, req)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *grpcClient) Ping(
|
func (a *grpcClient) Ping(
|
||||||
ctx context.Context, req *arkv1.PingRequest,
|
ctx context.Context, paymentID string,
|
||||||
) (*arkv1.PingResponse, error) {
|
) (*client.RoundFinalizationEvent, error) {
|
||||||
return a.svc.Ping(ctx, req)
|
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(
|
func (a *grpcClient) FinalizePayment(
|
||||||
ctx context.Context, req *arkv1.FinalizePaymentRequest,
|
ctx context.Context, signedForfeitTxs []string,
|
||||||
) (*arkv1.FinalizePaymentResponse, error) {
|
) error {
|
||||||
return a.svc.FinalizePayment(ctx, req)
|
req := &arkv1.FinalizePaymentRequest{
|
||||||
|
SignedForfeitTxs: signedForfeitTxs,
|
||||||
|
}
|
||||||
|
_, err := a.svc.FinalizePayment(ctx, req)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *grpcClient) GetRoundByID(
|
func (a *grpcClient) GetRoundByID(
|
||||||
ctx context.Context, roundID string,
|
ctx context.Context, roundID string,
|
||||||
) (*arkv1.GetRoundByIdResponse, error) {
|
) (*client.Round, error) {
|
||||||
return a.svc.GetRoundById(ctx, &arkv1.GetRoundByIdRequest{
|
req := &arkv1.GetRoundByIdRequest{Id: roundID}
|
||||||
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) {
|
type out client.Output
|
||||||
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
|
|
||||||
|
|
||||||
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))
|
nodes := make([]tree.Node, 0, len(level.Nodes))
|
||||||
|
|
||||||
for _, node := range level.Nodes {
|
for _, node := range level.Nodes {
|
||||||
@@ -258,7 +339,6 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
|||||||
Txid: node.Txid,
|
Txid: node.Txid,
|
||||||
Tx: node.Tx,
|
Tx: node.Tx,
|
||||||
ParentTxid: node.ParentTxid,
|
ParentTxid: node.ParentTxid,
|
||||||
Leaf: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,10 +348,39 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
|||||||
for j, treeLvl := range levels {
|
for j, treeLvl := range levels {
|
||||||
for i, node := range treeLvl {
|
for i, node := range treeLvl {
|
||||||
if len(levels.Children(node.Txid)) == 0 {
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/client"
|
"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"
|
||||||
"github.com/ark-network/ark-sdk/client/rest/service/arkservice/ark_service"
|
"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/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"
|
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
httptransport "github.com/go-openapi/runtime/client"
|
httptransport "github.com/go-openapi/runtime/client"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/vulpemventures/go-elements/psetv2"
|
"github.com/vulpemventures/go-elements/psetv2"
|
||||||
@@ -23,6 +25,7 @@ type restClient struct {
|
|||||||
svc ark_service.ClientService
|
svc ark_service.ClientService
|
||||||
eventsCh chan client.RoundEventChannel
|
eventsCh chan client.RoundEventChannel
|
||||||
requestTimeout time.Duration
|
requestTimeout time.Duration
|
||||||
|
treeCache *utils.Cache[tree.CongestionTree]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(aspUrl string) (client.ASPClient, error) {
|
func NewClient(aspUrl string) (client.ASPClient, error) {
|
||||||
@@ -35,14 +38,15 @@ func NewClient(aspUrl string) (client.ASPClient, error) {
|
|||||||
}
|
}
|
||||||
eventsCh := make(chan client.RoundEventChannel)
|
eventsCh := make(chan client.RoundEventChannel)
|
||||||
reqTimeout := 15 * time.Second
|
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 (c *restClient) Close() {}
|
||||||
|
|
||||||
func (a *restClient) GetEventStream(
|
func (a *restClient) GetEventStream(
|
||||||
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
|
ctx context.Context, paymentID string,
|
||||||
) (<-chan client.RoundEventChannel, error) {
|
) (<-chan client.RoundEventChannel, error) {
|
||||||
go func(payID string) {
|
go func(payID string) {
|
||||||
defer close(a.eventsCh)
|
defer close(a.eventsCh)
|
||||||
@@ -57,9 +61,7 @@ func (a *restClient) GetEventStream(
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
resp, err := a.Ping(ctx, &arkv1.PingRequest{
|
event, err := a.Ping(ctx, payID)
|
||||||
PaymentId: payID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.eventsCh <- client.RoundEventChannel{
|
a.eventsCh <- client.RoundEventChannel{
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -67,39 +69,13 @@ func (a *restClient) GetEventStream(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.GetEvent() != nil {
|
if event != 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
a.eventsCh <- client.RoundEventChannel{
|
a.eventsCh <- client.RoundEventChannel{
|
||||||
Event: &arkv1.GetEventStreamResponse{
|
Event: *event,
|
||||||
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(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
roundID := resp.GetEvent().GetId()
|
roundID := event.ID
|
||||||
round, err := a.GetRoundByID(ctx, roundID)
|
round, err := a.GetRoundByID(ctx, roundID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.eventsCh <- client.RoundEventChannel{
|
a.eventsCh <- client.RoundEventChannel{
|
||||||
@@ -108,29 +84,20 @@ func (a *restClient) GetEventStream(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if round.GetRound().GetStage() == arkv1.RoundStage_ROUND_STAGE_FINALIZED {
|
if round.Stage == client.RoundStageFinalized {
|
||||||
ptx, _ := psetv2.NewPsetFromBase64(round.GetRound().GetPoolTx())
|
|
||||||
utx, _ := ptx.UnsignedTx()
|
|
||||||
a.eventsCh <- client.RoundEventChannel{
|
a.eventsCh <- client.RoundEventChannel{
|
||||||
Event: &arkv1.GetEventStreamResponse{
|
Event: client.RoundFinalizedEvent{
|
||||||
Event: &arkv1.GetEventStreamResponse_RoundFinalized{
|
ID: roundID,
|
||||||
RoundFinalized: &arkv1.RoundFinalizedEvent{
|
Txid: getTxid(round.Tx),
|
||||||
PoolTxid: utx.TxHash().String(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if round.GetRound().GetStage() == arkv1.RoundStage_ROUND_STAGE_FAILED {
|
if round.Stage == client.RoundStageFailed {
|
||||||
a.eventsCh <- client.RoundEventChannel{
|
a.eventsCh <- client.RoundEventChannel{
|
||||||
Event: &arkv1.GetEventStreamResponse{
|
Event: client.RoundFailedEvent{
|
||||||
Event: &arkv1.GetEventStreamResponse_RoundFailed{
|
ID: roundID,
|
||||||
RoundFailed: &arkv1.RoundFailed{
|
|
||||||
Id: round.GetRound().GetId(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -150,7 +117,7 @@ func (a *restClient) GetEventStream(
|
|||||||
|
|
||||||
func (a *restClient) GetInfo(
|
func (a *restClient) GetInfo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) (*arkv1.GetInfoResponse, error) {
|
) (*client.Info, error) {
|
||||||
resp, err := a.svc.ArkServiceGetInfo(ark_service.NewArkServiceGetInfoParams())
|
resp, err := a.svc.ArkServiceGetInfo(ark_service.NewArkServiceGetInfoParams())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -176,7 +143,7 @@ func (a *restClient) GetInfo(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &arkv1.GetInfoResponse{
|
return &client.Info{
|
||||||
Pubkey: resp.Payload.Pubkey,
|
Pubkey: resp.Payload.Pubkey,
|
||||||
RoundLifetime: int64(roundLifetime),
|
RoundLifetime: int64(roundLifetime),
|
||||||
UnilateralExitDelay: int64(unilateralExitDelay),
|
UnilateralExitDelay: int64(unilateralExitDelay),
|
||||||
@@ -188,51 +155,76 @@ func (a *restClient) GetInfo(
|
|||||||
|
|
||||||
func (a *restClient) ListVtxos(
|
func (a *restClient) ListVtxos(
|
||||||
ctx context.Context, addr string,
|
ctx context.Context, addr string,
|
||||||
) (*arkv1.ListVtxosResponse, error) {
|
) ([]client.Vtxo, []client.Vtxo, error) {
|
||||||
resp, err := a.svc.ArkServiceListVtxos(
|
resp, err := a.svc.ArkServiceListVtxos(
|
||||||
ark_service.NewArkServiceListVtxosParams().WithAddress(addr),
|
ark_service.NewArkServiceListVtxosParams().WithAddress(addr),
|
||||||
)
|
)
|
||||||
if err != nil {
|
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 {
|
for _, v := range resp.Payload.SpendableVtxos {
|
||||||
|
var expiresAt *time.Time
|
||||||
|
if v.ExpireAt != "" && v.ExpireAt != "0" {
|
||||||
expAt, err := strconv.Atoi(v.ExpireAt)
|
expAt, err := strconv.Atoi(v.ExpireAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
t := time.Unix(int64(expAt), 0)
|
||||||
|
expiresAt = &t
|
||||||
}
|
}
|
||||||
|
|
||||||
amount, err := strconv.Atoi(v.Receiver.Amount)
|
amount, err := strconv.Atoi(v.Receiver.Amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
vtxos = append(vtxos, &arkv1.Vtxo{
|
spendableVtxos = append(spendableVtxos, client.Vtxo{
|
||||||
Outpoint: &arkv1.Input{
|
VtxoKey: client.VtxoKey{
|
||||||
Txid: v.Outpoint.Txid,
|
Txid: v.Outpoint.Txid,
|
||||||
Vout: uint32(v.Outpoint.Vout),
|
VOut: uint32(v.Outpoint.Vout),
|
||||||
},
|
},
|
||||||
Receiver: &arkv1.Output{
|
|
||||||
Address: v.Receiver.Address,
|
|
||||||
Amount: uint64(amount),
|
Amount: uint64(amount),
|
||||||
},
|
RoundTxid: v.PoolTxid,
|
||||||
Spent: v.Spent,
|
ExpiresAt: expiresAt,
|
||||||
PoolTxid: v.PoolTxid,
|
|
||||||
SpentBy: v.SpentBy,
|
|
||||||
ExpireAt: int64(expAt),
|
|
||||||
Swept: v.Swept,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return &arkv1.ListVtxosResponse{
|
spentVtxos := make([]client.Vtxo, 0, len(resp.Payload.SpentVtxos))
|
||||||
SpendableVtxos: vtxos,
|
for _, v := range resp.Payload.SpentVtxos {
|
||||||
}, nil
|
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(
|
func (a *restClient) GetRound(
|
||||||
ctx context.Context, txID string,
|
ctx context.Context, txID string,
|
||||||
) (*arkv1.GetRoundResponse, error) {
|
) (*client.Round, error) {
|
||||||
resp, err := a.svc.ArkServiceGetRound(
|
resp, err := a.svc.ArkServiceGetRound(
|
||||||
ark_service.NewArkServiceGetRoundParams().WithTxid(txID),
|
ark_service.NewArkServiceGetRoundParams().WithTxid(txID),
|
||||||
)
|
)
|
||||||
@@ -250,304 +242,125 @@ func (a *restClient) GetRound(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Round.CongestionTree.Levels))
|
startedAt := time.Unix(int64(start), 0)
|
||||||
for _, l := range resp.Payload.Round.CongestionTree.Levels {
|
var endedAt *time.Time
|
||||||
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
|
if end > 0 {
|
||||||
for _, n := range l.Nodes {
|
t := time.Unix(int64(end), 0)
|
||||||
nodes = append(nodes, &arkv1.Node{
|
endedAt = &t
|
||||||
Txid: n.Txid,
|
|
||||||
Tx: n.Tx,
|
|
||||||
ParentTxid: n.ParentTxid,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
levels = append(levels, &arkv1.TreeLevel{
|
|
||||||
Nodes: nodes,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &arkv1.GetRoundResponse{
|
return &client.Round{
|
||||||
Round: &arkv1.Round{
|
ID: resp.Payload.Round.ID,
|
||||||
Id: resp.Payload.Round.ID,
|
StartedAt: &startedAt,
|
||||||
Start: int64(start),
|
EndedAt: endedAt,
|
||||||
End: int64(end),
|
Tx: resp.Payload.Round.PoolTx,
|
||||||
PoolTx: resp.Payload.Round.PoolTx,
|
Tree: treeFromProto{resp.Payload.Round.CongestionTree}.parse(),
|
||||||
CongestionTree: &arkv1.Tree{
|
|
||||||
Levels: levels,
|
|
||||||
},
|
|
||||||
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
|
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
|
||||||
Connectors: resp.Payload.Round.Connectors,
|
Connectors: resp.Payload.Round.Connectors,
|
||||||
},
|
Stage: toRoundStage(*resp.Payload.Round.Stage),
|
||||||
}, nil
|
}, 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(
|
func (a *restClient) Onboard(
|
||||||
ctx context.Context, req *arkv1.OnboardRequest,
|
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
|
||||||
) (*arkv1.OnboardResponse, error) {
|
) 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,
|
|
||||||
}
|
|
||||||
body := models.V1OnboardRequest{
|
body := models.V1OnboardRequest{
|
||||||
BoardingTx: req.GetBoardingTx(),
|
BoardingTx: tx,
|
||||||
CongestionTree: &congestionTree,
|
CongestionTree: treeToProto(congestionTree).parse(),
|
||||||
UserPubkey: req.GetUserPubkey(),
|
UserPubkey: userPubkey,
|
||||||
}
|
}
|
||||||
_, err := a.svc.ArkServiceOnboard(
|
_, err := a.svc.ArkServiceOnboard(
|
||||||
ark_service.NewArkServiceOnboardParams().WithBody(&body),
|
ark_service.NewArkServiceOnboardParams().WithBody(&body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &arkv1.OnboardResponse{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *restClient) RegisterPayment(
|
func (a *restClient) RegisterPayment(
|
||||||
ctx context.Context, req *arkv1.RegisterPaymentRequest,
|
ctx context.Context, inputs []client.VtxoKey,
|
||||||
) (*arkv1.RegisterPaymentResponse, error) {
|
) (string, error) {
|
||||||
inputs := make([]*models.V1Input, 0, len(req.GetInputs()))
|
ins := make([]*models.V1Input, 0, len(inputs))
|
||||||
for _, i := range req.GetInputs() {
|
for _, i := range inputs {
|
||||||
inputs = append(inputs, &models.V1Input{
|
ins = append(ins, &models.V1Input{
|
||||||
Txid: i.GetTxid(),
|
Txid: i.Txid,
|
||||||
Vout: int64(i.GetVout()),
|
Vout: int64(i.VOut),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
body := models.V1RegisterPaymentRequest{
|
body := models.V1RegisterPaymentRequest{
|
||||||
Inputs: inputs,
|
Inputs: ins,
|
||||||
}
|
}
|
||||||
resp, err := a.svc.ArkServiceRegisterPayment(
|
resp, err := a.svc.ArkServiceRegisterPayment(
|
||||||
ark_service.NewArkServiceRegisterPaymentParams().WithBody(&body),
|
ark_service.NewArkServiceRegisterPaymentParams().WithBody(&body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &arkv1.RegisterPaymentResponse{
|
return resp.Payload.ID, nil
|
||||||
Id: resp.Payload.ID,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *restClient) ClaimPayment(
|
func (a *restClient) ClaimPayment(
|
||||||
ctx context.Context, req *arkv1.ClaimPaymentRequest,
|
ctx context.Context, paymentID string, outputs []client.Output,
|
||||||
) (*arkv1.ClaimPaymentResponse, error) {
|
) error {
|
||||||
outputs := make([]*models.V1Output, 0, len(req.GetOutputs()))
|
outs := make([]*models.V1Output, 0, len(outputs))
|
||||||
for _, o := range req.GetOutputs() {
|
for _, o := range outputs {
|
||||||
outputs = append(outputs, &models.V1Output{
|
outs = append(outs, &models.V1Output{
|
||||||
Address: o.GetAddress(),
|
Address: o.Address,
|
||||||
Amount: strconv.Itoa(int(o.GetAmount())),
|
Amount: strconv.Itoa(int(o.Amount)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
body := models.V1ClaimPaymentRequest{
|
body := models.V1ClaimPaymentRequest{
|
||||||
ID: req.GetId(),
|
ID: paymentID,
|
||||||
Outputs: outputs,
|
Outputs: outs,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := a.svc.ArkServiceClaimPayment(
|
_, err := a.svc.ArkServiceClaimPayment(
|
||||||
ark_service.NewArkServiceClaimPaymentParams().WithBody(&body),
|
ark_service.NewArkServiceClaimPaymentParams().WithBody(&body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &arkv1.ClaimPaymentResponse{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *restClient) Ping(
|
func (a *restClient) Ping(
|
||||||
ctx context.Context, req *arkv1.PingRequest,
|
ctx context.Context, paymentID string,
|
||||||
) (*arkv1.PingResponse, error) {
|
) (*client.RoundFinalizationEvent, error) {
|
||||||
r := ark_service.NewArkServicePingParams()
|
r := ark_service.NewArkServicePingParams()
|
||||||
r.SetPaymentID(req.GetPaymentId())
|
r.SetPaymentID(paymentID)
|
||||||
resp, err := a.svc.ArkServicePing(r)
|
resp, err := a.svc.ArkServicePing(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var event *arkv1.RoundFinalizationEvent
|
var event *client.RoundFinalizationEvent
|
||||||
if resp.Payload.Event != nil &&
|
if resp.Payload.Event != nil {
|
||||||
resp.Payload.Event.ID != "" &&
|
event = &client.RoundFinalizationEvent{
|
||||||
len(resp.Payload.Event.ForfeitTxs) > 0 &&
|
ID: resp.Payload.Event.ID,
|
||||||
len(resp.Payload.Event.CongestionTree.Levels) > 0 &&
|
Tx: resp.Payload.Event.PoolTx,
|
||||||
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,
|
|
||||||
ForfeitTxs: resp.Payload.Event.ForfeitTxs,
|
ForfeitTxs: resp.Payload.Event.ForfeitTxs,
|
||||||
CongestionTree: &arkv1.Tree{
|
Tree: treeFromProto{resp.Payload.Event.CongestionTree}.parse(),
|
||||||
Levels: levels,
|
|
||||||
},
|
|
||||||
Connectors: resp.Payload.Event.Connectors,
|
Connectors: resp.Payload.Event.Connectors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &arkv1.PingResponse{
|
return event, nil
|
||||||
ForfeitTxs: resp.Payload.ForfeitTxs,
|
|
||||||
Event: event,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *restClient) FinalizePayment(
|
func (a *restClient) FinalizePayment(
|
||||||
ctx context.Context, req *arkv1.FinalizePaymentRequest,
|
ctx context.Context, signedForfeitTxs []string,
|
||||||
) (*arkv1.FinalizePaymentResponse, error) {
|
) error {
|
||||||
|
req := &arkv1.FinalizePaymentRequest{
|
||||||
|
SignedForfeitTxs: signedForfeitTxs,
|
||||||
|
}
|
||||||
body := models.V1FinalizePaymentRequest{
|
body := models.V1FinalizePaymentRequest{
|
||||||
SignedForfeitTxs: req.GetSignedForfeitTxs(),
|
SignedForfeitTxs: req.GetSignedForfeitTxs(),
|
||||||
}
|
}
|
||||||
_, err := a.svc.ArkServiceFinalizePayment(
|
_, err := a.svc.ArkServiceFinalizePayment(
|
||||||
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
|
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &arkv1.FinalizePaymentResponse{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *restClient) GetRoundByID(
|
func (a *restClient) GetRoundByID(
|
||||||
ctx context.Context, roundID string,
|
ctx context.Context, roundID string,
|
||||||
) (*arkv1.GetRoundByIdResponse, error) {
|
) (*client.Round, error) {
|
||||||
resp, err := a.svc.ArkServiceGetRoundByID(
|
resp, err := a.svc.ArkServiceGetRoundByID(
|
||||||
ark_service.NewArkServiceGetRoundByIDParams().WithID(roundID),
|
ark_service.NewArkServiceGetRoundByIDParams().WithID(roundID),
|
||||||
)
|
)
|
||||||
@@ -565,36 +378,22 @@ func (a *restClient) GetRoundByID(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Round.CongestionTree.Levels))
|
startedAt := time.Unix(int64(start), 0)
|
||||||
for _, l := range resp.Payload.Round.CongestionTree.Levels {
|
var endedAt *time.Time
|
||||||
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
|
if end > 0 {
|
||||||
for _, n := range l.Nodes {
|
t := time.Unix(int64(end), 0)
|
||||||
nodes = append(nodes, &arkv1.Node{
|
endedAt = &t
|
||||||
Txid: n.Txid,
|
|
||||||
Tx: n.Tx,
|
|
||||||
ParentTxid: n.ParentTxid,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
levels = append(levels, &arkv1.TreeLevel{
|
|
||||||
Nodes: nodes,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stage := stageStrToInt(*resp.Payload.Round.Stage)
|
return &client.Round{
|
||||||
|
ID: resp.Payload.Round.ID,
|
||||||
return &arkv1.GetRoundByIdResponse{
|
StartedAt: &startedAt,
|
||||||
Round: &arkv1.Round{
|
EndedAt: endedAt,
|
||||||
Id: resp.Payload.Round.ID,
|
Tx: resp.Payload.Round.PoolTx,
|
||||||
Start: int64(start),
|
Tree: treeFromProto{resp.Payload.Round.CongestionTree}.parse(),
|
||||||
End: int64(end),
|
|
||||||
PoolTx: resp.Payload.Round.PoolTx,
|
|
||||||
CongestionTree: &arkv1.Tree{
|
|
||||||
Levels: levels,
|
|
||||||
},
|
|
||||||
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
|
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
|
||||||
Connectors: resp.Payload.Round.Connectors,
|
Connectors: resp.Payload.Round.Connectors,
|
||||||
Stage: arkv1.RoundStage(stage),
|
Stage: toRoundStage(*resp.Payload.Round.Stage),
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,48 +424,83 @@ func newRestClient(
|
|||||||
return svc.ArkService, nil
|
return svc.ArkService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stageStrToInt(stage models.V1RoundStage) int {
|
func toRoundStage(stage models.V1RoundStage) client.RoundStage {
|
||||||
switch stage {
|
switch stage {
|
||||||
case models.V1RoundStageROUNDSTAGEUNSPECIFIED:
|
|
||||||
return 0
|
|
||||||
case models.V1RoundStageROUNDSTAGEREGISTRATION:
|
case models.V1RoundStageROUNDSTAGEREGISTRATION:
|
||||||
return 1
|
return client.RoundStageRegistration
|
||||||
case models.V1RoundStageROUNDSTAGEFINALIZATION:
|
case models.V1RoundStageROUNDSTAGEFINALIZATION:
|
||||||
return 2
|
return client.RoundStageFinalization
|
||||||
case models.V1RoundStageROUNDSTAGEFINALIZED:
|
case models.V1RoundStageROUNDSTAGEFINALIZED:
|
||||||
return 3
|
return client.RoundStageFinalized
|
||||||
case models.V1RoundStageROUNDSTAGEFAILED:
|
case models.V1RoundStageROUNDSTAGEFAILED:
|
||||||
return 4
|
return client.RoundStageFailed
|
||||||
|
default:
|
||||||
|
return client.RoundStageUndefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1
|
type treeFromProto struct {
|
||||||
|
*models.V1Tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
func (t treeFromProto) parse() tree.CongestionTree {
|
||||||
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
congestionTree = append(congestionTree, level)
|
||||||
|
}
|
||||||
|
|
||||||
for _, level := range treeFromProto.Levels {
|
for j, treeLvl := range congestionTree {
|
||||||
nodes := make([]tree.Node, 0, len(level.Nodes))
|
for i, node := range treeLvl {
|
||||||
|
if len(congestionTree.Children(node.Txid)) == 0 {
|
||||||
for _, node := range level.Nodes {
|
congestionTree[j][i] = tree.Node{
|
||||||
nodes = append(nodes, tree.Node{
|
|
||||||
Txid: node.Txid,
|
Txid: node.Txid,
|
||||||
Tx: node.Tx,
|
Tx: node.Tx,
|
||||||
ParentTxid: node.ParentTxid,
|
ParentTxid: node.ParentTxid,
|
||||||
Leaf: false,
|
Leaf: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
levels = append(levels, nodes)
|
Nodes: nodes,
|
||||||
}
|
})
|
||||||
|
|
||||||
for j, treeLvl := range levels {
|
|
||||||
for i, node := range treeLvl {
|
|
||||||
if len(levels.Children(node.Txid)) == 0 {
|
|
||||||
levels[j][i].Leaf = true
|
|
||||||
}
|
}
|
||||||
|
return &models.V1Tree{
|
||||||
|
Levels: levels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return levels, nil
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
1364
pkg/client-sdk/covenant_client.go
Normal file
1364
pkg/client-sdk/covenant_client.go
Normal file
File diff suppressed because it is too large
Load Diff
1456
pkg/client-sdk/covenantless_client.go
Normal file
1456
pkg/client-sdk/covenantless_client.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -100,10 +100,7 @@ func main() {
|
|||||||
|
|
||||||
amount := uint64(1000)
|
amount := uint64(1000)
|
||||||
receivers := []arksdk.Receiver{
|
receivers := []arksdk.Receiver{
|
||||||
{
|
arksdk.NewLiquidReceiver(bobOffchainAddr, amount),
|
||||||
To: bobOffchainAddr,
|
|
||||||
Amount: amount,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
@@ -145,7 +142,7 @@ func setupArkClient() (arksdk.ArkClient, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup store: %s", err)
|
return nil, fmt.Errorf("failed to setup store: %s", err)
|
||||||
}
|
}
|
||||||
client, err := arksdk.New(storeSvc)
|
client, err := arksdk.NewCovenantClient(storeSvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup ark client: %s", err)
|
return nil, fmt.Errorf("failed to setup ark client: %s", err)
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,11 @@ module ark/pkg/client-sdk/example
|
|||||||
|
|
||||||
go 1.22.2
|
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
|
require github.com/ark-network/ark-sdk v0.0.0-00010101000000-000000000000
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
async function initWallet() {
|
async function initWallet() {
|
||||||
try {
|
try {
|
||||||
|
const chain = "liquid";
|
||||||
const walletType = "singlekey"
|
const walletType = "singlekey"
|
||||||
const clientType = "rest"
|
const clientType = "rest"
|
||||||
const privateKey = document.getElementById("prvkey").value;
|
const privateKey = document.getElementById("prvkey").value;
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
logMessage("Init error: asp url is required");
|
logMessage("Init error: asp url is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await init(walletType, clientType, aspUrl, privateKey, password);
|
await init(walletType, clientType, aspUrl, privateKey, password, chain);
|
||||||
logMessage("wallet initialized and connected to ASP");
|
logMessage("wallet initialized and connected to ASP");
|
||||||
await config();
|
await config();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
console.log("unlocking...");
|
console.log("unlocking...");
|
||||||
await unlock(password);
|
await unlock(password);
|
||||||
console.log(amount, password);
|
console.log(amount, password);
|
||||||
|
console.log("onboarding...");
|
||||||
const txID = await onboard(amount);
|
const txID = await onboard(amount);
|
||||||
logMessage("Onboarded with amount: " + amount + " and txID: " + txID + ", if in regtest mine a block");
|
logMessage("Onboarded with amount: " + amount + " and txID: " + txID + ", if in regtest mine a block");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -18,7 +18,7 @@ func main() {
|
|||||||
|
|
||||||
store, _ := arksdkwasm.NewLocalStorageStore()
|
store, _ := arksdkwasm.NewLocalStorageStore()
|
||||||
if store != nil {
|
if store != nil {
|
||||||
if err := arksdkwasm.New(ctx, store); err != nil {
|
if err := arksdkwasm.NewCovenantClient(ctx, store); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -27,7 +27,7 @@ func main() {
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := arksdkwasm.New(ctx, storeSvc); err != nil {
|
if err := arksdkwasm.NewCovenantClient(ctx, storeSvc); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
231
pkg/client-sdk/example/covenantless/alice_to_bob.go
Normal file
231
pkg/client-sdk/example/covenantless/alice_to_bob.go
Normal 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
|
||||||
|
}
|
||||||
25
pkg/client-sdk/example/covenantless/wasm/README.md
Normal file
25
pkg/client-sdk/example/covenantless/wasm/README.md
Normal 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
|
||||||
|
```
|
||||||
61
pkg/client-sdk/example/covenantless/wasm/go.mod
Normal file
61
pkg/client-sdk/example/covenantless/wasm/go.mod
Normal 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
|
||||||
|
)
|
||||||
206
pkg/client-sdk/example/covenantless/wasm/go.sum
Normal file
206
pkg/client-sdk/example/covenantless/wasm/go.sum
Normal 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=
|
||||||
176
pkg/client-sdk/example/covenantless/wasm/index.html
Normal file
176
pkg/client-sdk/example/covenantless/wasm/index.html
Normal 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>
|
||||||
34
pkg/client-sdk/example/covenantless/wasm/main.go
Normal file
34
pkg/client-sdk/example/covenantless/wasm/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,24 @@
|
|||||||
package explorer
|
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 (
|
const (
|
||||||
BitcoinExplorer = "bitcoin"
|
BitcoinExplorer = "bitcoin"
|
||||||
@@ -11,7 +29,7 @@ type Utxo struct {
|
|||||||
Txid string `json:"txid"`
|
Txid string `json:"txid"`
|
||||||
Vout uint32 `json:"vout"`
|
Vout uint32 `json:"vout"`
|
||||||
Amount uint64 `json:"value"`
|
Amount uint64 `json:"value"`
|
||||||
Asset string `json:"asset"`
|
Asset string `json:"asset,omitempty"`
|
||||||
Status struct {
|
Status struct {
|
||||||
Confirmed bool `json:"confirmed"`
|
Confirmed bool `json:"confirmed"`
|
||||||
Blocktime int64 `json:"block_time"`
|
Blocktime int64 `json:"block_time"`
|
||||||
@@ -29,6 +47,304 @@ type Explorer interface {
|
|||||||
GetTxBlockTime(
|
GetTxBlockTime(
|
||||||
txid string,
|
txid string,
|
||||||
) (confirmed bool, blocktime int64, err error)
|
) (confirmed bool, blocktime int64, err error)
|
||||||
GetNetwork() network.Network
|
// GetNetwork() common.Network
|
||||||
BaseUrl() string
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package client
|
package redemption
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark-sdk/client"
|
||||||
"github.com/ark-network/ark-sdk/explorer"
|
"github.com/ark-network/ark-sdk/explorer"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
@@ -12,8 +13,8 @@ import (
|
|||||||
"github.com/vulpemventures/go-elements/taproot"
|
"github.com/vulpemventures/go-elements/taproot"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedeemBranch struct {
|
type CovenantRedeemBranch struct {
|
||||||
vtxo *Vtxo
|
vtxo client.Vtxo
|
||||||
branch []*psetv2.Pset
|
branch []*psetv2.Pset
|
||||||
internalKey *secp256k1.PublicKey
|
internalKey *secp256k1.PublicKey
|
||||||
sweepClosure *taproot.TapElementsLeaf
|
sweepClosure *taproot.TapElementsLeaf
|
||||||
@@ -21,11 +22,11 @@ type RedeemBranch struct {
|
|||||||
explorer explorer.Explorer
|
explorer explorer.Explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRedeemBranch(
|
func NewCovenantRedeemBranch(
|
||||||
explorer explorer.Explorer,
|
explorer explorer.Explorer,
|
||||||
congestionTree tree.CongestionTree, vtxo *Vtxo,
|
congestionTree tree.CongestionTree, vtxo client.Vtxo,
|
||||||
) (*RedeemBranch, error) {
|
) (*CovenantRedeemBranch, error) {
|
||||||
sweepClosure, seconds, err := findSweepClosure(congestionTree)
|
sweepClosure, seconds, err := findCovenantSweepClosure(congestionTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ func NewRedeemBranch(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &RedeemBranch{
|
return &CovenantRedeemBranch{
|
||||||
vtxo: vtxo,
|
vtxo: vtxo,
|
||||||
branch: branch,
|
branch: branch,
|
||||||
internalKey: internalKey,
|
internalKey: internalKey,
|
||||||
@@ -66,10 +67,10 @@ func NewRedeemBranch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
|
// 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))
|
transactions := make([]string, 0, len(r.branch))
|
||||||
|
|
||||||
offchainPath, err := r.OffchainPath()
|
offchainPath, err := r.offchainPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ func (r *RedeemBranch) RedeemPath() ([]string, error) {
|
|||||||
return transactions, nil
|
return transactions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RedeemBranch) ExpiresAt() (*time.Time, error) {
|
func (r *CovenantRedeemBranch) ExpiresAt() (*time.Time, error) {
|
||||||
lastKnownBlocktime := int64(0)
|
lastKnownBlocktime := int64(0)
|
||||||
|
|
||||||
confirmed, blocktime, _ := r.explorer.GetTxBlockTime(r.vtxo.RoundTxid)
|
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
|
// 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...)
|
offchainPath := append([]*psetv2.Pset{}, r.branch...)
|
||||||
|
|
||||||
for i := len(r.branch) - 1; i >= 0; i-- {
|
for i := len(r.branch) - 1; i >= 0; i-- {
|
||||||
@@ -180,7 +181,7 @@ func (r *RedeemBranch) OffchainPath() ([]*psetv2.Pset, error) {
|
|||||||
return offchainPath, nil
|
return offchainPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findSweepClosure(
|
func findCovenantSweepClosure(
|
||||||
congestionTree tree.CongestionTree,
|
congestionTree tree.CongestionTree,
|
||||||
) (*taproot.TapElementsLeaf, uint, error) {
|
) (*taproot.TapElementsLeaf, uint, error) {
|
||||||
root, err := congestionTree.Root()
|
root, err := congestionTree.Root()
|
||||||
193
pkg/client-sdk/internal/utils/redemption/covenantless_redeem.go
Normal file
193
pkg/client-sdk/internal/utils/redemption/covenantless_redeem.go
Normal 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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/client"
|
"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 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,82 +1,29 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/client"
|
"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"
|
||||||
"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/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/vulpemventures/go-elements/address"
|
"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(
|
func CoinSelect(
|
||||||
vtxos []*client.Vtxo, amount, dust uint64, sortByExpirationTime bool,
|
vtxos []client.Vtxo, amount, dust uint64, sortByExpirationTime bool,
|
||||||
) ([]*client.Vtxo, uint64, error) {
|
) ([]client.Vtxo, uint64, error) {
|
||||||
selected := make([]*client.Vtxo, 0)
|
selected := make([]client.Vtxo, 0)
|
||||||
notSelected := make([]*client.Vtxo, 0)
|
notSelected := make([]client.Vtxo, 0)
|
||||||
selectedAmount := uint64(0)
|
selectedAmount := uint64(0)
|
||||||
|
|
||||||
if sortByExpirationTime {
|
if sortByExpirationTime {
|
||||||
@@ -131,7 +78,7 @@ func DecodeReceiverAddress(addr string) (
|
|||||||
return true, outputScript, nil, nil
|
return true, outputScript, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsOnchainOnly(receivers []*arkv1.Output) bool {
|
func IsOnchainOnly(receivers []client.Output) bool {
|
||||||
for _, receiver := range receivers {
|
for _, receiver := range receivers {
|
||||||
isOnChain, _, _, err := DecodeReceiverAddress(receiver.Address)
|
isOnChain, _, _, err := DecodeReceiverAddress(receiver.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -146,66 +93,6 @@ func IsOnchainOnly(receivers []*arkv1.Output) bool {
|
|||||||
return true
|
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 {
|
func NetworkFromString(net string) common.Network {
|
||||||
switch net {
|
switch net {
|
||||||
case common.Liquid.Name:
|
case common.Liquid.Name:
|
||||||
@@ -224,3 +111,131 @@ func NetworkFromString(net string) common.Network {
|
|||||||
return common.Bitcoin
|
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
|
||||||
|
}
|
||||||
|
|||||||
293
pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go
Normal file
293
pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +1,43 @@
|
|||||||
package liquidwallet
|
package singlekeywallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/explorer"
|
"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/store"
|
||||||
"github.com/ark-network/ark-sdk/wallet"
|
"github.com/ark-network/ark-sdk/wallet"
|
||||||
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
|
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"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
||||||
"github.com/vulpemventures/go-elements/payment"
|
"github.com/vulpemventures/go-elements/payment"
|
||||||
"github.com/vulpemventures/go-elements/psetv2"
|
"github.com/vulpemventures/go-elements/psetv2"
|
||||||
"github.com/vulpemventures/go-elements/transaction"
|
"github.com/vulpemventures/go-elements/transaction"
|
||||||
)
|
)
|
||||||
|
|
||||||
type singlekeyWallet struct {
|
type liquidWallet struct {
|
||||||
configStore store.ConfigStore
|
*singlekeyWallet
|
||||||
walletStore walletstore.WalletStore
|
|
||||||
privateKey *secp256k1.PrivateKey
|
|
||||||
walletData *walletstore.WalletData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWalletService(
|
func NewLiquidWallet(
|
||||||
configStore store.ConfigStore, walletStore walletstore.WalletStore,
|
configStore store.ConfigStore, walletStore walletstore.WalletStore,
|
||||||
) (wallet.WalletService, error) {
|
) (wallet.WalletService, error) {
|
||||||
walletData, err := walletStore.GetWallet()
|
walletData, err := walletStore.GetWallet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &singlekeyWallet{configStore, walletStore, nil, walletData}, nil
|
return &liquidWallet{
|
||||||
|
&singlekeyWallet{configStore, walletStore, nil, walletData},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *singlekeyWallet) GetType() string {
|
func (w *liquidWallet) GetAddresses(
|
||||||
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(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) ([]string, []string, []string, error) {
|
) ([]string, []string, []string, error) {
|
||||||
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
|
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
|
||||||
@@ -151,7 +51,7 @@ func (w *singlekeyWallet) GetAddresses(
|
|||||||
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
|
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *singlekeyWallet) NewAddress(
|
func (w *liquidWallet) NewAddress(
|
||||||
ctx context.Context, _ bool,
|
ctx context.Context, _ bool,
|
||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
||||||
@@ -161,7 +61,7 @@ func (w *singlekeyWallet) NewAddress(
|
|||||||
return offchainAddr, onchainAddr, nil
|
return offchainAddr, onchainAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *singlekeyWallet) NewAddresses(
|
func (w *liquidWallet) NewAddresses(
|
||||||
ctx context.Context, _ bool, num int,
|
ctx context.Context, _ bool, num int,
|
||||||
) ([]string, []string, error) {
|
) ([]string, []string, error) {
|
||||||
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
||||||
@@ -178,7 +78,7 @@ func (w *singlekeyWallet) NewAddresses(
|
|||||||
return offchainAddrs, onchainAddrs, nil
|
return offchainAddrs, onchainAddrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singlekeyWallet) SignTransaction(
|
func (s *liquidWallet) SignTransaction(
|
||||||
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
pset, err := psetv2.NewPsetFromBase64(tx)
|
pset, err := psetv2.NewPsetFromBase64(tx)
|
||||||
@@ -234,7 +134,7 @@ func (s *singlekeyWallet) SignTransaction(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
liquidNet := toElementsNetwork(storeData.Network)
|
liquidNet := utils.ToElementsNetwork(storeData.Network)
|
||||||
p2wpkh := payment.FromPublicKey(s.walletData.Pubkey, &liquidNet, nil)
|
p2wpkh := payment.FromPublicKey(s.walletData.Pubkey, &liquidNet, nil)
|
||||||
onchainWalletScript := p2wpkh.WitnessScript
|
onchainWalletScript := p2wpkh.WitnessScript
|
||||||
|
|
||||||
@@ -355,7 +255,7 @@ func (s *singlekeyWallet) SignTransaction(
|
|||||||
return pset.ToBase64()
|
return pset.ToBase64()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *singlekeyWallet) getAddress(
|
func (w *liquidWallet) getAddress(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) (string, string, string, error) {
|
) (string, string, string, error) {
|
||||||
if w.walletData == nil {
|
if w.walletData == nil {
|
||||||
@@ -374,7 +274,7 @@ func (w *singlekeyWallet) getAddress(
|
|||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
liquidNet := toElementsNetwork(data.Network)
|
liquidNet := utils.ToElementsNetwork(data.Network)
|
||||||
|
|
||||||
p2wpkh := payment.FromPublicKey(w.walletData.Pubkey, &liquidNet, nil)
|
p2wpkh := payment.FromPublicKey(w.walletData.Pubkey, &liquidNet, nil)
|
||||||
onchainAddr, err := p2wpkh.WitnessPubKeyHash()
|
onchainAddr, err := p2wpkh.WitnessPubKeyHash()
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
118
pkg/client-sdk/wallet/singlekey/wallet.go
Normal file
118
pkg/client-sdk/wallet/singlekey/wallet.go
Normal 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
|
||||||
|
}
|
||||||
@@ -2,13 +2,14 @@ package wallet_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ark-network/ark-sdk/client"
|
"github.com/ark-network/ark-sdk/client"
|
||||||
"github.com/ark-network/ark-sdk/store"
|
"github.com/ark-network/ark-sdk/store"
|
||||||
inmemorystore "github.com/ark-network/ark-sdk/store/inmemory"
|
inmemorystore "github.com/ark-network/ark-sdk/store/inmemory"
|
||||||
"github.com/ark-network/ark-sdk/wallet"
|
"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"
|
inmemorywalletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store/inmemory"
|
||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
@@ -35,10 +36,15 @@ func TestWallet(t *testing.T) {
|
|||||||
args []interface{}
|
args []interface{}
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: wallet.SingleKeyWallet,
|
name: "liquid" + wallet.SingleKeyWallet,
|
||||||
chain: "liquid",
|
chain: "liquid",
|
||||||
args: []interface{}{common.LiquidRegTest},
|
args: []interface{}{common.LiquidRegTest},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "bitcoin" + wallet.SingleKeyWallet,
|
||||||
|
chain: "bitcoin",
|
||||||
|
args: []interface{}{common.LiquidRegTest},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range tests {
|
for i := range tests {
|
||||||
@@ -59,7 +65,9 @@ func TestWallet(t *testing.T) {
|
|||||||
|
|
||||||
var walletSvc wallet.WalletService
|
var walletSvc wallet.WalletService
|
||||||
if tt.chain == "liquid" {
|
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.NoError(t, err)
|
||||||
require.NotNil(t, walletSvc)
|
require.NotNil(t, walletSvc)
|
||||||
@@ -85,7 +93,7 @@ func TestWallet(t *testing.T) {
|
|||||||
require.NotEmpty(t, onchainAddr)
|
require.NotEmpty(t, onchainAddr)
|
||||||
|
|
||||||
expectedNumOfAddresses := 2
|
expectedNumOfAddresses := 2
|
||||||
if tt.name == wallet.SingleKeyWallet {
|
if strings.Contains(tt.name, wallet.SingleKeyWallet) {
|
||||||
expectedNumOfAddresses = 1
|
expectedNumOfAddresses = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +110,7 @@ func TestWallet(t *testing.T) {
|
|||||||
require.Len(t, onchainAddrs, num)
|
require.Len(t, onchainAddrs, num)
|
||||||
|
|
||||||
expectedNumOfAddresses += num
|
expectedNumOfAddresses += num
|
||||||
if tt.name == wallet.SingleKeyWallet {
|
if strings.Contains(tt.name, wallet.SingleKeyWallet) {
|
||||||
expectedNumOfAddresses = 1
|
expectedNumOfAddresses = 1
|
||||||
}
|
}
|
||||||
offchainAddrs, onchainAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx)
|
offchainAddrs, onchainAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
arksdk "github.com/ark-network/ark-sdk"
|
arksdk "github.com/ark-network/ark-sdk"
|
||||||
"github.com/ark-network/ark-sdk/store"
|
"github.com/ark-network/ark-sdk/store"
|
||||||
"github.com/ark-network/ark-sdk/wallet"
|
"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"
|
walletstore "github.com/ark-network/ark-sdk/wallet/singlekey/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func init() {
|
|||||||
js.Global().Set("getMinRelayFee", GetMinRelayFeeWrapper())
|
js.Global().Set("getMinRelayFee", GetMinRelayFeeWrapper())
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func NewCovenantClient(
|
||||||
ctx context.Context, storeSvc store.ConfigStore,
|
ctx context.Context, storeSvc store.ConfigStore,
|
||||||
) error {
|
) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -52,7 +52,7 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if data == nil {
|
if data == nil {
|
||||||
arkSdkClient, err = arksdk.New(storeSvc)
|
arkSdkClient, err = arksdk.NewCovenantClient(storeSvc)
|
||||||
} else {
|
} else {
|
||||||
var walletSvc wallet.WalletService
|
var walletSvc wallet.WalletService
|
||||||
switch data.WalletType {
|
switch data.WalletType {
|
||||||
@@ -65,7 +65,42 @@ func New(
|
|||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown wallet type")
|
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 {
|
if err != nil {
|
||||||
js.Global().Get("console").Call("error", err.Error())
|
js.Global().Get("console").Call("error", err.Error())
|
||||||
@@ -92,8 +127,7 @@ func getSingleKeyWallet(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.Contains(network, "liquid") {
|
if strings.Contains(network, "liquid") {
|
||||||
return liquidwallet.NewWalletService(configStore, walletStore)
|
return singlekeywallet.NewLiquidWallet(configStore, walletStore)
|
||||||
}
|
}
|
||||||
// TODO: Support bitcoin wallet
|
return singlekeywallet.NewBitcoinWallet(configStore, walletStore)
|
||||||
return nil, fmt.Errorf("network %s not supported yet", network)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
arksdk "github.com/ark-network/ark-sdk"
|
arksdk "github.com/ark-network/ark-sdk"
|
||||||
"github.com/ark-network/ark-sdk/wallet"
|
"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 {
|
func LogWrapper() js.Func {
|
||||||
@@ -25,9 +25,13 @@ func logMsg(msg string) {
|
|||||||
|
|
||||||
func InitWrapper() js.Func {
|
func InitWrapper() js.Func {
|
||||||
return JSPromise(func(args []js.Value) (interface{}, error) {
|
return JSPromise(func(args []js.Value) (interface{}, error) {
|
||||||
if len(args) != 5 {
|
if len(args) != 6 {
|
||||||
return nil, errors.New("invalid number of args")
|
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
|
var walletSvc wallet.WalletService
|
||||||
switch args[0].String() {
|
switch args[0].String() {
|
||||||
@@ -36,10 +40,17 @@ func InitWrapper() js.Func {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to init wallet store: %s", err)
|
return nil, fmt.Errorf("failed to init wallet store: %s", err)
|
||||||
}
|
}
|
||||||
walletSvc, err = liquidwallet.NewWalletService(configStore, walletStore)
|
if chain == "liquid" {
|
||||||
|
walletSvc, err = singlekeywallet.NewLiquidWallet(configStore, walletStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
walletSvc, err = singlekeywallet.NewBitcoinWallet(configStore, walletStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported wallet type")
|
return nil, fmt.Errorf("unsupported wallet type")
|
||||||
}
|
}
|
||||||
@@ -150,10 +161,9 @@ func SendOnChainWrapper() js.Func {
|
|||||||
receivers := make([]arksdk.Receiver, args[0].Length())
|
receivers := make([]arksdk.Receiver, args[0].Length())
|
||||||
for i := 0; i < args[0].Length(); i++ {
|
for i := 0; i < args[0].Length(); i++ {
|
||||||
receiver := args[0].Index(i)
|
receiver := args[0].Index(i)
|
||||||
receivers[i] = arksdk.Receiver{
|
receivers[i] = arksdk.NewLiquidReceiver(
|
||||||
To: receiver.Get("To").String(),
|
receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()),
|
||||||
Amount: uint64(receiver.Get("Amount").Int()),
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
txID, err := arkSdkClient.SendOnChain(
|
txID, err := arkSdkClient.SendOnChain(
|
||||||
@@ -175,10 +185,9 @@ func SendOffChainWrapper() js.Func {
|
|||||||
receivers := make([]arksdk.Receiver, args[1].Length())
|
receivers := make([]arksdk.Receiver, args[1].Length())
|
||||||
for i := 0; i < args[1].Length(); i++ {
|
for i := 0; i < args[1].Length(); i++ {
|
||||||
receiver := args[1].Index(i)
|
receiver := args[1].Index(i)
|
||||||
receivers[i] = arksdk.Receiver{
|
receivers[i] = arksdk.NewLiquidReceiver(
|
||||||
To: receiver.Get("To").String(),
|
receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()),
|
||||||
Amount: uint64(receiver.Get("Amount").Int()),
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
txID, err := arkSdkClient.SendOffChain(
|
txID, err := arkSdkClient.SendOffChain(
|
||||||
|
|||||||
@@ -497,12 +497,10 @@ func (s *covenantlessService) finalizeRound() {
|
|||||||
log.Debugf("signing round transaction %s\n", round.Id)
|
log.Debugf("signing round transaction %s\n", round.Id)
|
||||||
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
|
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
|
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
|
||||||
log.WithError(err).Warn("failed to sign round tx")
|
log.WithError(err).Warn("failed to sign round tx")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println(signedPoolTx)
|
|
||||||
|
|
||||||
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
|
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ func (s *sweeper) createTask(
|
|||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
// sweepableVtxos related to the sweep input
|
// sweepableVtxos related to the sweep input
|
||||||
sweepableVtxos := make([]domain.VtxoKey, 0)
|
sweepableVtxos := make([]domain.VtxoKey, 0)
|
||||||
fmt.Println("input", input.GetHash().String(), input.GetIndex())
|
|
||||||
|
|
||||||
// check if input is the vtxo itself
|
// check if input is the vtxo itself
|
||||||
vtxos, _ := s.repoManager.Vtxos().GetVtxos(
|
vtxos, _ := s.repoManager.Vtxos().GetVtxos(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package txbuilder
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
@@ -34,8 +33,6 @@ func sweepTransaction(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("sweepClosure.Pubkey", hex.EncodeToString(sweepClosure.Pubkey.SerializeCompressed()))
|
|
||||||
|
|
||||||
if !valid {
|
if !valid {
|
||||||
return nil, fmt.Errorf("invalid csv script")
|
return nil, fmt.Errorf("invalid csv script")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: @louisinger shall we delete this code?
|
||||||
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet)
|
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet)
|
||||||
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
|
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
|
||||||
|
|
||||||
@@ -110,8 +111,6 @@ func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
|
|||||||
// return nil, err
|
// return nil, err
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// fmt.Println("PREIMAGE", hex.EncodeToString(preimage))
|
|
||||||
|
|
||||||
return s.wallet.SignPsbt(packet)
|
return s.wallet.SignPsbt(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user