From a956c918bb7fb0918b1eed959d5d46002c180b85 Mon Sep 17 00:00:00 2001 From: Marco Bardelli Date: Fri, 14 Oct 2022 00:04:07 +0200 Subject: [PATCH 1/6] added support for stake addresses --- address.go | 49 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/address.go b/address.go index 5f7fba8..acea8ce 100644 --- a/address.go +++ b/address.go @@ -15,6 +15,7 @@ const ( Base AddressType = 0x00 Ptr AddressType = 0x04 Enterprise AddressType = 0x06 + Stake AddressType = 0x0e ) // Address represents a Cardano address. @@ -94,7 +95,7 @@ func NewAddressFromBytes(bytes []byte) (Address, error) { } case Ptr: if len(bytes) <= 29 { - return addr, errors.New("enterprise address length should be greater than 29") + return addr, errors.New("pointer address length should be greater than 29") } index := uint(29) @@ -120,7 +121,7 @@ func NewAddressFromBytes(bytes []byte) (Address, error) { addr.Pointer = Pointer{Slot: slot, TxIndex: txIndex, CertIndex: certIndex} case Ptr + 1: if len(bytes) <= 29 { - return addr, errors.New("enterprise address length should be greater than 29") + return addr, errors.New("pointer address length should be greater than 29") } index := uint(29) @@ -160,6 +161,22 @@ func NewAddressFromBytes(bytes []byte) (Address, error) { Type: ScriptCredential, ScriptHash: bytes[1:29], } + case Stake: + if len(bytes) != 29 { + return addr, errors.New("stake address length should be 29") + } + addr.Stake = StakeCredential{ + Type: KeyCredential, + KeyHash: bytes[1:29], + } + case Stake + 1: + if len(bytes) != 29 { + return addr, errors.New("stake address length should be 29") + } + addr.Stake = StakeCredential{ + Type: ScriptCredential, + ScriptHash: bytes[1:29], + } } return addr, nil @@ -208,6 +225,8 @@ func (addr *Address) Bytes() []byte { addrBytes = append(addrBytes, addr.Stake.Hash()...) case Enterprise, Enterprise + 1: addrBytes = append(addrBytes, addr.Payment.Hash()...) + case Stake, Stake + 1: + addrBytes = append(addrBytes, addr.Stake.Hash()...) case Ptr, Ptr + 1: addrBytes = append(addrBytes, addr.Payment.Hash()...) addrBytes = append(addrBytes, encodeToNat(addr.Pointer.Slot)...) @@ -220,7 +239,7 @@ func (addr *Address) Bytes() []byte { // Bech32 returns the Address encoded as bech32. func (addr *Address) Bech32() string { - addrStr, err := bech32.EncodeFromBase256(getHrp(addr.Network), addr.Bytes()) + addrStr, err := bech32.EncodeFromBase256(getHrp(addr.Network, addr.Type), addr.Bytes()) if err != nil { panic(err) } @@ -254,6 +273,15 @@ func NewEnterpriseAddress(network Network, payment StakeCredential) (Address, er return Address{Type: addrType, Network: network, Payment: payment}, nil } +// NewStakeAddress returns a new Staake Address. +func NewStakeAddress(network Network, stake StakeCredential) (Address, error) { + addrType := Stake + if stake.Type == ScriptCredential { + addrType = Stake + 1 + } + return Address{Type: addrType, Network: network, Stake: stake}, nil +} + // Pointer is the location of the Stake Registration Certificate in the blockchain. type Pointer struct { Slot uint64 @@ -316,11 +344,20 @@ func Blake224Hash(b []byte) ([]byte, error) { return hash.Sum(nil), err } -func getHrp(network Network) string { +func getHrp(network Network, atyp ...AddressType) string { + prefix := "addr" + suffix := "" switch network { case Testnet, Preprod: - return "addr_test" + suffix = "_test" default: - return "addr" } + if len(atyp) == 1 { + switch atyp[0] { + case Stake: + prefix = "stake" + default: + } + } + return prefix + suffix } From 01ade10ba5efeeae4c59cdc5255d03289f5b1eef Mon Sep 17 00:00:00 2001 From: Marco Bardelli Date: Fri, 14 Oct 2022 00:07:14 +0200 Subject: [PATCH 2/6] expose bech32 module for easy marshalling --- bech32/bech32.go | 122 ++++++++++++++++++++++++++++++++++++++++ bech32/prefixes/cip5.go | 91 ++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 bech32/bech32.go create mode 100644 bech32/prefixes/cip5.go diff --git a/bech32/bech32.go b/bech32/bech32.go new file mode 100644 index 0000000..35c0f1d --- /dev/null +++ b/bech32/bech32.go @@ -0,0 +1,122 @@ +package bech32 + +import ( + "fmt" + + "github.com/echovl/cardano-go/bech32/prefixes" + "github.com/echovl/cardano-go/internal/bech32" +) + +type Bech32Prefix = prefixes.Bech32Prefix + +var ( + EncodeWithPrefix = bech32.Encode + Decode = bech32.Decode + DecodeNoLimit = bech32.DecodeNoLimit + + EncodeFromBase256WithPrefix = bech32.EncodeFromBase256 + DecodeToBase256 = bech32.DecodeToBase256 +) + +type ( + Bech32Codec interface { + Prefix() string + Bytes() []byte + SetBytes([]byte) + Len() int + } + Bech32Encoder interface { + Prefix() string + Bytes() []byte + } +) + +func Encode(args ...any) (string, error) { + var hrp string + var data []byte + switch len(args) { + case 1: + // the argument have to be a Bech32Codec + if c, ok := args[0].(Bech32Encoder); !ok { + return "", fmt.Errorf("Wrong parameter: %T is not a Bech32Encoder", c) + } + hrp = args[0].(Bech32Encoder).Prefix() + data = args[0].(Bech32Encoder).Bytes() + case 2: + // the argument haave to be a Bech32Codec or a string, and the second have to be a []byte + a1 := args[0] + a2 := args[1] + + switch a1.(type) { + case Bech32Encoder: + hrp = a1.(Bech32Codec).Prefix() + case string: + hrp = a1.(string) + default: + return "", fmt.Errorf("Wrong 1st parameter: %T is not a string or Bech32Codec", a1) + } + + if _, ok := a2.([]byte); !ok { + return "", fmt.Errorf("Wrong 2nd parameter: %T is not a []byte", a2) + } + data = a2.([]byte) + } + return bech32.Encode(hrp, data) +} + +func EncodeFromBase256(args ...any) (string, error) { + var hrp string + var data []byte + switch len(args) { + case 1: + // the argument have to be a Bech32Codec + if c, ok := args[0].(Bech32Encoder); !ok { + return "", fmt.Errorf("Wrong parameter: %T is not a Bech32Encoder", c) + } + hrp = args[0].(Bech32Encoder).Prefix() + if converted, err := bech32.ConvertBits(args[0].(Bech32Encoder).Bytes(), 8, 5, true); err != nil { + return "", err + } else { + data = converted + } + case 2: + // the argument haave to be a Bech32Codec or a string, and the second have to be a []byte + a1 := args[0] + a2 := args[1] + + switch a1.(type) { + case Bech32Encoder: + hrp = a1.(Bech32Encoder).Prefix() + case string: + hrp = a1.(string) + default: + return "", fmt.Errorf("Wrong 1st parameter: %T is not a string or Bech32Codec", a1) + } + + if _, ok := a2.([]byte); !ok { + return "", fmt.Errorf("Wrong 2nd parameter: %T is not a []byte", a2) + } + if converted, err := bech32.ConvertBits(a2.([]byte), 8, 5, true); err != nil { + } else { + data = converted + } + } + return bech32.Encode(hrp, data) +} + +func DecodeInto(be32 string, codec Bech32Codec) error { + expectedLen := codec.Len() + expectedPrefix := codec.Prefix() + hrp, data, err := bech32.DecodeToBase256(be32) + if err != nil { + return err + } + if hrp != expectedPrefix { + return fmt.Errorf("Wrong prefix: want %s got %s", expectedPrefix, hrp) + } + codec.SetBytes(data) + if len(codec.Bytes()) != expectedLen || string(codec.Bytes()) != string(data) { + return fmt.Errorf("Set bytes failed") + } + return nil +} diff --git a/bech32/prefixes/cip5.go b/bech32/prefixes/cip5.go new file mode 100644 index 0000000..9eae84f --- /dev/null +++ b/bech32/prefixes/cip5.go @@ -0,0 +1,91 @@ +package prefixes + +// As specified in [CIP-5](https://github.com/cardano-foundation/CIPs/tree/master/CIP5) +// +// copied from cardano-addresses core/lib/Cardano/Codec/Bech32/Prefixes.hs + +type Bech32Prefix = string + +const ( + // -- * Addresses + + Addr Bech32Prefix = "addr" + AddrTest Bech32Prefix = "addr_test" + Script Bech32Prefix = "script" + Stake Bech32Prefix = "stake" + StakeTest Bech32Prefix = "stake_test" + + // -- * Hashes + + AddrPublicKeyHash Bech32Prefix = "addr_vkh" + StakePublicKeyHash Bech32Prefix = "stake_vkh" + AddrSharedPublicKeyHash Bech32Prefix = "addr_shared_vkh" + StakeSharedPublicKeyHash Bech32Prefix = "stake_shared_vkh" + + // -- * Keys for 1852H + AddrPublicKey Bech32Prefix = "addr_vk" + AddrPrivateKey Bech32Prefix = "addr_sk" + AddrXPub Bech32Prefix = "addr_xvk" + AddrXPrv Bech32Prefix = "addr_xsk" + AddrExtendedPublicKey = AddrXPub + AddrExtendedPrivateKey = AddrXPrv + + AcctPublicKey Bech32Prefix = "acct_vk" + AcctPrivateKey Bech32Prefix = "acct_sk" + AcctXPub Bech32Prefix = "acct_xvk" + AcctXPrv Bech32Prefix = "acct_xsk" + AcctExtendedPublicKey = AcctXPub + AcctExtendedPrivateKey = AcctXPrv + + RootPublicKey Bech32Prefix = "root_vk" + RootPrivateKey Bech32Prefix = "root_sk" + RootXPub Bech32Prefix = "root_xvk" + RootXPrv Bech32Prefix = "root_xsk" + RootExtendedPublicKey = RootXPub + RootExtendedPrivateKey = RootXPrv + + StakePublicKey Bech32Prefix = "stake_vk" + StakePrivateKey Bech32Prefix = "stake_sk" + StakeXPub Bech32Prefix = "stake_xvk" + StakeXPrv Bech32Prefix = "stake_xsk" + StakeExtendedPublicKey = StakeXPub + StakeExtendedPrivateKey = StakeXPrv + + // -- * Keys for 1854H + + AddrSharedPublicKey Bech32Prefix = "addr_shared_vk" + AddrSharedPrivateKey Bech32Prefix = "addr_shared_sk" + AddrSharedXPub Bech32Prefix = "addr_shared_xvk" + AddrSharedXPrv Bech32Prefix = "addr_shared_xsk" + AddrSharedExtendedPublicKey = AddrSharedXPub + AddrSharedExtendedPrivateKey = AddrSharedXPrv + + AcctSharedPublicKey Bech32Prefix = "acct_shared_vk" + AcctSharedPrivateKey Bech32Prefix = "acct_shared_sk" + AcctSharedXPub Bech32Prefix = "acct_shared_xvk" + AcctSharedXPrv Bech32Prefix = "acct_shared_xsk" + AcctSharedExtendedPublicKey = AcctSharedXPub + AcctSharedExtendedPrivateKey = AcctSharedXPrv + + RootSharedPublicKey Bech32Prefix = "root_shared_vk" + RootSharedPrivateKey Bech32Prefix = "root_shared_sk" + RootSharedXPub Bech32Prefix = "root_shared_xvk" + RootSharedXPrv Bech32Prefix = "root_shared_xsk" + RootSharedExtendedPublicKey = RootSharedXPub + RootSharedExtendedPrivateKey = RootSharedXPrv + + StakeSharedPublicKey Bech32Prefix = "stake_shared_vk" + StakeSharedPrivateKey Bech32Prefix = "stake_shared_sk" + StakeSharedXPub Bech32Prefix = "stake_shared_xvk" + StakeSharedXPrv Bech32Prefix = "stake_shared_xsk" + StakeSharedExtendedPublicKey = StakeSharedXPub + StakeSharedExtendedPrivateKey = StakeSharedXPrv + + // -- * Keys for 1855H + PolicyPublicKey Bech32Prefix = "policy_vk" + PolicyPrivateKey Bech32Prefix = "policy_sk" + PolicyXPub Bech32Prefix = "policy_xvk" + PolicyXPrv Bech32Prefix = "policy_xsk" + PolicyExtendedPublicKey = PolicyXPub + PolicyExtendedPrivateKey = PolicyXPrv +) From fc363cf1c1261d90aff37329cbc71f01a88e4be5 Mon Sep 17 00:00:00 2001 From: Marco Bardelli Date: Sun, 16 Oct 2022 07:25:49 +0200 Subject: [PATCH 3/6] add stake credential constructor from key hash --- credential.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/credential.go b/credential.go index ccc1134..3086f8a 100644 --- a/credential.go +++ b/credential.go @@ -50,6 +50,14 @@ func NewKeyCredential(publicKey crypto.PubKey) (StakeCredential, error) { return StakeCredential{Type: KeyCredential, KeyHash: keyHash}, nil } +// NewKeyCredential creates a Key Credential from an AddrKeyHash (28 bytes key hash). +func NewKeyCredentialFromHash(keyHash AddrKeyHash) (StakeCredential, error) { + if len(keyHash) < 28 { + return StakeCredential{}, fmt.Errorf("Wrong argument: expected 28 bytes key hash") + } + return StakeCredential{Type: KeyCredential, KeyHash: keyHash[:28]}, nil +} + // NewKeyCredential creates a Script Credential. func NewScriptCredential(script []byte) (StakeCredential, error) { scriptHash, err := Blake224Hash(script) From 7f705bdf8bcef7b0e4d5ee8c5bb318f0ca9d9997 Mon Sep 17 00:00:00 2001 From: Marco Bardelli Date: Fri, 21 Oct 2022 00:36:16 +0200 Subject: [PATCH 4/6] Allow to set additional witnesses for partially signed tx preparation --- tx_builder.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tx_builder.go b/tx_builder.go index 556db99..fc46fd9 100644 --- a/tx_builder.go +++ b/tx_builder.go @@ -15,6 +15,8 @@ type TxBuilder struct { pkeys []crypto.PrvKey changeReceiver *Address + + additionalWitnesses uint } // NewTxBuilder returns a new instance of TxBuilder. @@ -48,6 +50,12 @@ func (tb *TxBuilder) SetFee(fee Coin) { tb.tx.Body.Fee = fee } +// SetAdditionalWitnesses sets future witnesses for a partially signed transction. +// This is useful to compute the real length and so fee in advance +func (tb *TxBuilder) SetAdditionalWitnesses(witnesses uint) { + tb.additionalWitnesses = witnesses +} + // AddAuxiliaryData adds auxiliary data to the transaction. func (tb *TxBuilder) AddAuxiliaryData(data *AuxiliaryData) { tb.tx.AuxiliaryData = data @@ -143,6 +151,9 @@ func (tb *TxBuilder) MinCoinsForTxOut(txOut *TxOutput) Coin { func (tb *TxBuilder) calculateMinFee() Coin { txBytes := tb.tx.Bytes() txLength := uint64(len(txBytes)) + // for each additional witnesses there will be an additional 100 bytes, + // (32 public key, 64 signature, 4 index/key in cbor) + txLength += uint64(tb.additionalWitnesses * 100) return tb.protocol.MinFeeA*Coin(txLength) + tb.protocol.MinFeeB } From 2e6f45080b1169e59f0df4df552d72da99c79947 Mon Sep 17 00:00:00 2001 From: Marco Bardelli Date: Mon, 24 Oct 2022 12:35:31 +0200 Subject: [PATCH 5/6] Change calculateMinFee to increase it a bit in case of additionalWitnesses is still unclear how to precisely calculate the bytes for witnesses --- tx_builder.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tx_builder.go b/tx_builder.go index fc46fd9..7e0792f 100644 --- a/tx_builder.go +++ b/tx_builder.go @@ -154,6 +154,11 @@ func (tb *TxBuilder) calculateMinFee() Coin { // for each additional witnesses there will be an additional 100 bytes, // (32 public key, 64 signature, 4 index/key in cbor) txLength += uint64(tb.additionalWitnesses * 100) + // apparently that is not enough, so just consider 1 additional byte + // for each additional witness after the first + if tb.additionalWitnesses > 1 { + txLength += uint64(tb.additionalWitnesses - 1) + } return tb.protocol.MinFeeA*Coin(txLength) + tb.protocol.MinFeeB } From 45d384746b3a24306cfda383e0252d12dddd414d Mon Sep 17 00:00:00 2001 From: Marco Bardelli Date: Sat, 12 Nov 2022 13:01:30 +0100 Subject: [PATCH 6/6] expose cardano-cli DoCommand method --- cardano-cli/cli.go | 69 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/cardano-cli/cli.go b/cardano-cli/cli.go index da0e1cd..9110ab7 100644 --- a/cardano-cli/cli.go +++ b/cardano-cli/cli.go @@ -1,7 +1,7 @@ package cardanocli import ( - "bytes" + "context" "encoding/hex" "encoding/json" "errors" @@ -12,11 +12,44 @@ import ( "strconv" "strings" + flag "github.com/spf13/pflag" + "github.com/echovl/cardano-go" ) +var socketPath, fallbackSocketPath string + +func AddFlags(fs *flag.FlagSet) { + fs.StringVar(&socketPath, "cardano-node-socket-path", "", "") + fs.StringVar(&fallbackSocketPath, "fallback-cardano-node-socket-path", "", "") +} + +func availableAsSocket(fn string) bool { + fi, err := os.Stat(fn) + if err != nil { + return false + } + if (fi.Mode().Type() & os.ModeSocket) != os.ModeSocket { + return false + } + return true +} + +func getSocketPathToUse() string { + sp := socketPath + if sp != "" && availableAsSocket(sp) { + return sp + } + sp = fallbackSocketPath + if sp != "" && availableAsSocket(sp) { + return sp + } + return os.Getenv("CARDANO_NODE_SOCKET_PATH") +} + // CardanoCli implements Node using cardano-cli and a local node. type CardanoCli struct { + ctx context.Context network cardano.Network } @@ -35,12 +68,15 @@ type cliTx struct { } // NewNode returns a new instance of CardanoCli. -func NewNode(network cardano.Network) cardano.Node { +func NewNode(network cardano.Network, rest ...any) cardano.Node { + if len(rest) > 0 { + return &CardanoCli{network: network, ctx: rest[0].(context.Context)} + } return &CardanoCli{network: network} } -func (c *CardanoCli) runCommand(args ...string) ([]byte, error) { - out := &bytes.Buffer{} +func (c *CardanoCli) runCommand(args ...string) (string, error) { + out := &strings.Builder{} if c.network == cardano.Mainnet { args = append(args, "--mainnet") @@ -48,14 +84,23 @@ func (c *CardanoCli) runCommand(args ...string) ([]byte, error) { args = append(args, "--testnet-magic", strconv.Itoa(cardano.ProtocolMagic)) } - cmd := exec.Command("cardano-cli", args...) + var cmd *exec.Cmd + if c.ctx == nil { + cmd = exec.Command("cardano-cli", args...) + } else { + cmd = exec.CommandContext(c.ctx, "cardano-cli", args...) + } cmd.Stdout = out cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return nil, err + return "", err } - return out.Bytes(), nil + return out.String(), nil +} + +func (c *CardanoCli) DoCommand(args ...string) (string, error) { + return c.runCommand(args...) } func (c *CardanoCli) UTxOs(addr cardano.Address) ([]cardano.UTxO, error) { @@ -65,7 +110,7 @@ func (c *CardanoCli) UTxOs(addr cardano.Address) ([]cardano.UTxO, error) { } utxos := []cardano.UTxO{} - lines := strings.Split(string(out), "\n") + lines := strings.Split(out, "\n") if len(lines) < 3 { return utxos, nil @@ -142,7 +187,7 @@ func (c *CardanoCli) Tip() (*cardano.NodeTip, error) { } cliTip := &tip{} - if err = json.Unmarshal(out, cliTip); err != nil { + if err = json.Unmarshal([]byte(out), cliTip); err != nil { return nil, err } @@ -171,7 +216,7 @@ func (c *CardanoCli) SubmitTx(tx *cardano.Tx) (*cardano.Hash32, error) { out, err := c.runCommand("transaction", "submit", "--tx-file", txFile.Name()) if err != nil { - return nil, errors.New(string(out)) + return nil, errors.New(out) } txHash, err := tx.Hash() @@ -191,11 +236,11 @@ type protocolParameters struct { func (c *CardanoCli) ProtocolParams() (*cardano.ProtocolParams, error) { out, err := c.runCommand("query", "protocol-parameters") if err != nil { - return nil, errors.New(string(out)) + return nil, errors.New(out) } var cparams protocolParameters - if err := json.Unmarshal(out, &cparams); err != nil { + if err := json.Unmarshal([]byte(out), &cparams); err != nil { return nil, err }