diff --git a/cmd/runestonecli/address.go b/cmd/runestonecli/address.go new file mode 100644 index 0000000..306d08c --- /dev/null +++ b/cmd/runestonecli/address.go @@ -0,0 +1,77 @@ +package main + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" +) + +func GetTapScriptAddress(pk *btcec.PublicKey, revealedScript []byte, net *chaincfg.Params) (btcutil.Address, error) { + pubkey33 := pk.SerializeCompressed() + if pubkey33[0] == 0x02 { + pubkey33[0] = byte(txscript.BaseLeafVersion) + } else { + pubkey33[0] = byte(txscript.BaseLeafVersion) + 1 + } + + controlBlock, err := txscript.ParseControlBlock( + pubkey33, + ) + if err != nil { + return nil, err + } + rootHash := controlBlock.RootHash(revealedScript) + + // Next, we'll construct the final commitment (creating the external or + // taproot output key) as a function of this commitment and the + // included internal key: taprootKey = internalKey + (tPoint*G). + taprootKey := txscript.ComputeTaprootOutputKey( + controlBlock.InternalKey, rootHash, + ) + + // If we convert the taproot key to a witness program (we just need to + // serialize the public key), then it should exactly match the witness + // program passed in. + tapKeyBytes := schnorr.SerializePubKey(taprootKey) + + addr, err := btcutil.NewAddressTaproot( + tapKeyBytes, + net, + ) + return addr, nil +} +func GetTaprootPubkey(pubkey *btcec.PublicKey, revealedScript []byte) (*btcec.PublicKey, error) { + controlBlock := txscript.ControlBlock{} + controlBlock.InternalKey = pubkey + rootHash := controlBlock.RootHash(revealedScript) + + // Next, we'll construct the final commitment (creating the external or + // taproot output key) as a function of this commitment and the + // included internal key: taprootKey = internalKey + (tPoint*G). + taprootKey := txscript.ComputeTaprootOutputKey( + controlBlock.InternalKey, rootHash, + ) + return taprootKey, nil +} + +// GetP2TRAddress returns a taproot address for a given public key. +func GetP2TRAddress(pubKey *btcec.PublicKey, net *chaincfg.Params) (string, error) { + addr, err := getP2TRAddress(pubKey, net) + if err != nil { + return "", err + + } + return addr.EncodeAddress(), nil +} +func getP2TRAddress(pubKey *btcec.PublicKey, net *chaincfg.Params) (btcutil.Address, error) { + tapKey := txscript.ComputeTaprootKeyNoScript(pubKey) + addr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(tapKey), net, + ) + if err != nil { + return nil, err + } + return addr, nil +} diff --git a/cmd/runestonecli/config.go b/cmd/runestonecli/config.go new file mode 100644 index 0000000..cd2cf75 --- /dev/null +++ b/cmd/runestonecli/config.go @@ -0,0 +1,186 @@ +package main + +import ( + "encoding/hex" + "errors" + "unicode/utf8" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/bxelab/runestone" + "lukechampine.com/uint128" +) + +type Config struct { + PrivateKey string + FeePerByte int64 + UtxoAmount int64 + Network string + RpcUrl string + Etching *struct { + Rune string + Symbol *string + Premine *uint64 + Amount *uint64 + Cap *uint64 + Divisibility *int + HeightStart *int + HeightEnd *int + HeightOffsetStart *int + HeightOffsetEnd *int + } + Mint *struct { + RuneId string + } +} + +func DefaultConfig() Config { + return Config{ + FeePerByte: 5, + UtxoAmount: 1000, + Network: "mainnet", + RpcUrl: "https://mempool.space/api", + } + +} +func (c Config) GetFeePerByte() int64 { + if c.FeePerByte == 0 { + return 5 + } + return c.FeePerByte +} +func (c Config) GetUtxoAmount() int64 { + if c.UtxoAmount == 0 { + return 666 + } + return c.UtxoAmount +} + +func (c Config) GetEtching() (*runestone.Etching, error) { + if c.Etching == nil { + return nil, errors.New("Etching config is required") + } + if c.Etching.Rune == "" { + return nil, errors.New("Rune is required") + } + if c.Etching.Symbol != nil { + runeCount := utf8.RuneCountInString(*c.Etching.Symbol) + if runeCount != 1 { + return nil, errors.New("Symbol must be a single character") + } + } + etching := &runestone.Etching{} + r, err := runestone.SpacedRuneFromString(c.Etching.Rune) + if err != nil { + return nil, err + } + etching.Rune = &r.Rune + etching.Spacers = &r.Spacers + if c.Etching.Symbol != nil { + symbolStr := *c.Etching.Symbol + symbol := rune(symbolStr[0]) + etching.Symbol = &symbol + } + if c.Etching.Premine != nil { + premine := uint128.From64(*c.Etching.Premine) + etching.Premine = &premine + } + if c.Etching.Amount != nil { + amount := uint128.From64(*c.Etching.Amount) + if etching.Terms == nil { + etching.Terms = &runestone.Terms{} + } + etching.Terms.Amount = &amount + } + if c.Etching.Cap != nil { + cap := uint128.From64(*c.Etching.Cap) + etching.Terms.Cap = &cap + } + if c.Etching.Divisibility != nil { + d := uint8(*c.Etching.Divisibility) + etching.Divisibility = &d + } + if c.Etching.HeightStart != nil { + h := uint64(*c.Etching.HeightStart) + if etching.Terms == nil { + etching.Terms = &runestone.Terms{} + } + etching.Terms.Height[0] = &h + } + if c.Etching.HeightEnd != nil { + h := uint64(*c.Etching.HeightEnd) + if etching.Terms == nil { + etching.Terms = &runestone.Terms{} + } + etching.Terms.Height[1] = &h + } + if c.Etching.HeightOffsetStart != nil { + h := uint64(*c.Etching.HeightOffsetStart) + if etching.Terms == nil { + etching.Terms = &runestone.Terms{} + } + etching.Terms.Offset[0] = &h + } + if c.Etching.HeightOffsetEnd != nil { + h := uint64(*c.Etching.HeightOffsetEnd) + if etching.Terms == nil { + etching.Terms = &runestone.Terms{} + } + etching.Terms.Offset[1] = &h + } + return etching, nil +} +func (c Config) GetMint() (*runestone.RuneId, error) { + if c.Mint == nil { + return nil, errors.New("Mint config is required") + } + if c.Mint.RuneId == "" { + return nil, errors.New("RuneId is required") + } + runeId, err := runestone.RuneIdFromString(c.Mint.RuneId) + if err != nil { + return nil, err + } + return runeId, nil +} +func (c Config) GetNetwork() *chaincfg.Params { + if c.Network == "mainnet" { + return &chaincfg.MainNetParams + } + if c.Network == "testnet" { + return &chaincfg.TestNet3Params + } + if c.Network == "regtest" { + return &chaincfg.RegressionNetParams + } + if c.Network == "signet" { + return &chaincfg.SigNetParams + } + panic("unknown network") +} + +func (c Config) GetPrivateKeyAddr() (*btcec.PrivateKey, string, error) { + if c.PrivateKey == "" { + return nil, "", errors.New("PrivateKey is required") + } + pkBytes, err := hex.DecodeString(c.PrivateKey) + if err != nil { + return nil, "", err + } + privKey, pubKey := btcec.PrivKeyFromBytes(pkBytes) + if err != nil { + return nil, "", err + } + tapKey := txscript.ComputeTaprootKeyNoScript(pubKey) + addr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(tapKey), c.GetNetwork(), + ) + if err != nil { + return nil, "", err + } + address := addr.EncodeAddress() + return privKey, address, nil +} diff --git a/cmd/runestonecli/config.yaml b/cmd/runestonecli/config.yaml new file mode 100644 index 0000000..9592051 --- /dev/null +++ b/cmd/runestonecli/config.yaml @@ -0,0 +1,18 @@ +PrivateKey: "1234567890" +Network: "testnet" # mainnet or testnet +RpcUrl: "https://blockstream.info/testnet/api" #https://mempool.space/api https://mempool.space/testnet/api +FeePerByte: 5 +UtxoAmount: 1000 +Etching: + Rune: "STUDYZY" + Symbol: "曾" + Premine: 1000000 + Amount: 1000 + Cap: 20000 +# Divisibility: 0 +# HeightStart: 0 +# HeightEnd: 0 +# HeightOffsetStart: 0 +# HeightOffsetEnd: 0 +Mint: + RuneId: "2609649:946" \ No newline at end of file diff --git a/cmd/runestonecli/example.go b/cmd/runestonecli/example.go new file mode 100644 index 0000000..f942b84 --- /dev/null +++ b/cmd/runestonecli/example.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/bxelab/runestone" + "lukechampine.com/uint128" +) + +func testEtching() { + runeName := "STUDYZY.GMAIL.COM" + symbol := '曾' + myRune, err := runestone.SpacedRuneFromString(runeName) + if err != nil { + fmt.Println(err) + return + } + amt := uint128.From64(666666) + ca := uint128.From64(21000000) + etching := &runestone.Etching{ + Rune: &myRune.Rune, + Spacers: &myRune.Spacers, + Symbol: &symbol, + Terms: &runestone.Terms{ + Amount: &amt, + Cap: &ca, + }, + } + r := runestone.Runestone{Etching: etching} + data, err := r.Encipher() + if err != nil { + fmt.Println(err) + } + fmt.Printf("Etching data: 0x%x\n", data) + dataString, _ := txscript.DisasmString(data) + fmt.Printf("Etching Script: %s\n", dataString) +} +func testMint() { + runeIdStr := "2609649:946" + runeId, _ := runestone.RuneIdFromString(runeIdStr) + r := runestone.Runestone{Mint: runeId} + data, err := r.Encipher() + if err != nil { + fmt.Println(err) + } + fmt.Printf("Mint Rune[%s] data: 0x%x\n", runeIdStr, data) + dataString, _ := txscript.DisasmString(data) + fmt.Printf("Mint Script: %s\n", dataString) +} +func testDecode() { + data, _ := hex.DecodeString("140114001600") //Mint UNCOMMON•GOODS + var tx wire.MsgTx + builder := txscript.NewScriptBuilder() + // Push opcode OP_RETURN + builder.AddOp(txscript.OP_RETURN) + // Push MAGIC_NUMBER + builder.AddOp(runestone.MAGIC_NUMBER) + // Push payload + builder.AddData(data) + pkScript, _ := builder.Script() + txOut := wire.NewTxOut(0, pkScript) + tx.AddTxOut(txOut) + r := &runestone.Runestone{} + artifact, err := r.Decipher(&tx) + if err != nil { + fmt.Println(err) + return + } + a, _ := json.Marshal(artifact) + fmt.Printf("Artifact: %s\n", string(a)) +} diff --git a/cmd/runestonecli/go.mod b/cmd/runestonecli/go.mod index 61664ac..22f8cff 100644 --- a/cmd/runestonecli/go.mod +++ b/cmd/runestonecli/go.mod @@ -2,20 +2,44 @@ module github.com/bxelab/runestone/cmd/runestonecli go 1.22.2 -require github.com/bxelab/runestone v0.0.0-20240425113004-bea3419a6a3e +require ( + github.com/btcsuite/btcd v0.24.0 + github.com/btcsuite/btcd/btcec/v2 v2.1.3 + github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 + github.com/bxelab/runestone v0.0.0-20240425113004-bea3419a6a3e + github.com/manifoldco/promptui v0.9.0 + github.com/pkg/errors v0.9.1 + github.com/spf13/viper v1.18.2 + golang.org/x/text v0.14.0 + lukechampine.com/uint128 v1.3.0 +) require ( - github.com/btcsuite/btcd v0.24.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/aead/siphash v1.0.1 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect - golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect - lukechampine.com/uint128 v1.3.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/sys v0.19.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace ( - github.com/bxelab/runestone => ../../ -) \ No newline at end of file + +replace github.com/bxelab/runestone => ../../ diff --git a/cmd/runestonecli/go.sum b/cmd/runestonecli/go.sum index 36835af..6fe409e 100644 --- a/cmd/runestonecli/go.sum +++ b/cmd/runestonecli/go.sum @@ -1,3 +1,4 @@ +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= @@ -25,19 +26,28 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku 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/bxelab/runestone v0.0.0-20240425113004-bea3419a6a3e h1:DU0AKtAJeQbHr/xbZT6qSMwRdIhaCXECS6/oRqNaoc8= -github.com/bxelab/runestone v0.0.0-20240425113004-bea3419a6a3e/go.mod h1:pieyLaoNj2JsfnMzZm9McEZmGU804aTBsRQNTwqeqVk= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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= @@ -45,16 +55,32 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU 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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -64,17 +90,52 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 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 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 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= @@ -82,6 +143,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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= @@ -89,11 +151,14 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w 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 h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -104,7 +169,11 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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= diff --git a/cmd/runestonecli/hash.go b/cmd/runestonecli/hash.go new file mode 100644 index 0000000..87a8ca7 --- /dev/null +++ b/cmd/runestonecli/hash.go @@ -0,0 +1,62 @@ +package main + +import "encoding/hex" + +const HashLength = 32 + +type Hash [HashLength]byte + +var ZeroHash = Hash{} + +// BytesToHash sets b to hash. +// If b is larger than len(h), b will be cropped from the left. +func BytesToHash(b []byte) Hash { + var h Hash + h.SetBytes(b) + return h +} +func (h *Hash) SetBytes(b []byte) { + if len(b) > len(h) { + b = b[len(b)-HashLength:] + } + + copy(h[HashLength-len(b):], b) +} + +// BtcString returns the Hash as the hexadecimal string of the byte-reversed hash. +func (hash Hash) BtcString() string { + for i := 0; i < HashLength/2; i++ { + hash[i], hash[HashLength-1-i] = hash[HashLength-1-i], hash[i] + } + return hex.EncodeToString(hash[:]) +} +func (h Hash) String() string { + return hex.EncodeToString(h[:]) +} +func HexToHash(s string) Hash { + + return BytesToHash(FromHex(s)) +} + +// FromHex returns the bytes represented by the hexadecimal string s. +// s may be prefixed with "0x". +func FromHex(s string) []byte { + if has0xPrefix(s) { + s = s[2:] + } + if len(s)%2 == 1 { + s = "0" + s + } + return Hex2Bytes(s) +} + +// has0xPrefix validates str begins with '0x' or '0X'. +func has0xPrefix(str string) bool { + return len(str) >= 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X') +} + +// Hex2Bytes returns the bytes represented by the hexadecimal string str. +func Hex2Bytes(str string) []byte { + h, _ := hex.DecodeString(str) + return h +} diff --git a/cmd/runestonecli/i18n.go b/cmd/runestonecli/i18n.go new file mode 100644 index 0000000..e163ab0 --- /dev/null +++ b/cmd/runestonecli/i18n.go @@ -0,0 +1,66 @@ +package main + +import ( + "os" + "strings" + + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var lang = getDefaultLanguage() + +func init() { + initString("Please select an option", "请选择一个选项") + initString("Etching a new rune", "发行新的符文") + initString("Mint rune", "挖掘已定义的符文") + initString("Prompt failed %v", "提示错误:%v") + initString("Fatal error config file: %s", "config文件读取错误:%s") + initString("Unable to unmarshal config: %s", "config文件解码错误:%s") + initString("Private key error:", "私钥配置错误:") + initString("Your address is: ", "您的地址是:") + initString("Etching rune encipher error:", "发行符文配置有误") + initString("Etching:%s, data:%x", "符文配置:%s, 编码后数据:%x") + initString("BuildRuneEtchingTxs error:", "发行符文交易构建错误") + initString("commit Tx: %x\n", "提交交易: %x\n") + initString("reveal Tx: %x\n", "揭示交易: %x\n") + initString("SendTx", "发送交易") + initString("WriteTxToFile", "写入交易到文件") + initString("How to process the transaction?", "如何处理交易?") + initString("SendRawTransaction error:", "发送原始交易错误:") + initString("committed tx hash:", "已提交,交易哈希:") + initString("waiting for confirmations..., please don't close the program.", "等待确认中,确认数必须大于6之后才能发送揭示交易。请勿关闭程序。") + initString("GetTransaction error:", "获取交易错误:") + initString("commit tx confirmations:", "提交交易确认数:") + initString("Etch complete, reveal tx hash:", "发行完成,揭示交易哈希:") + initString("create file tx.txt error:", "创建交易文件tx.txt错误:") + initString("write to file tx.txt", "写入交易到文件tx.txt") + initString("Mint Rune[%s] data: 0x%x\n", "挖掘符文[%s] 数据: 0x%x\n") + initString("BuildMintRuneTx error:", "构建挖掘符文交易错误:") + initString("mint rune tx: %x\n", "挖掘符文交易: %x\n") +} +func initString(english, chinese string) { + key := english + message.SetString(language.English, key, english) + message.SetString(language.Chinese, key, chinese) +} +func i18n(key string) string { + str := message.NewPrinter(lang).Sprintf(key) + if len(str) == 0 { + return key + } + return str +} + +func getDefaultLanguage() language.Tag { + langEnv := os.Getenv("LANG") + if langEnv == "" { + langEnv = os.Getenv("LANGUAGE") + } + langTag := strings.Split(langEnv, ".")[0] + tag, err := language.Parse(langTag) + if err != nil { + return language.Chinese + } + return tag +} diff --git a/cmd/runestonecli/main.go b/cmd/runestonecli/main.go index fc72583..7354de3 100644 --- a/cmd/runestonecli/main.go +++ b/cmd/runestonecli/main.go @@ -1,80 +1,223 @@ package main import ( - "encoding/hex" + "bytes" "encoding/json" - "fmt" + "os" + "sync" + "time" - "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/bxelab/runestone" - "lukechampine.com/uint128" + "github.com/manifoldco/promptui" + "github.com/spf13/viper" + "golang.org/x/text/message" ) +var config = DefaultConfig() +var p *message.Printer + func main() { - testEtching() - testMint() - testDecode() + p = message.NewPrinter(lang) + loadConfig() + checkAndPrintConfig() + + // 显示多语言文本 + items := []string{i18n("Etching a new rune"), i18n("Mint rune")} + prompt := promptui.Select{ + Label: i18n("Please select an option"), + Items: items, + } + + optionIdx, _, err := prompt.Run() + + if err != nil { + p.Printf("Prompt failed %v", err) + return + } + if optionIdx == 0 { //Etching a new rune + BuildEtchingTxs() + } + if optionIdx == 1 { //Mint rune + BuildMintTxs() + + } +} + +func loadConfig() { + viper.SetConfigName("config") + viper.AddConfigPath(".") + err := viper.ReadInConfig() + if err != nil { + panic(p.Sprintf("Fatal error config file: %s", err)) + } + + err = viper.Unmarshal(&config) + if err != nil { + panic(p.Sprintf("Unable to unmarshal config: %s", err)) + } +} +func checkAndPrintConfig() { + //check privatekey and print address + _, addr, err := config.GetPrivateKeyAddr() + if err != nil { + p.Println("Private key error:", err.Error()) + return + } + p.Println("Your address is: ", addr) + +} +func BuildEtchingTxs() { + etching, err := config.GetEtching() + if err != nil { + p.Println("error:", err.Error()) + return + } + rs := runestone.Runestone{Etching: etching} + data, err := rs.Encipher() + if err != nil { + p.Println("Etching rune encipher error:", err.Error()) + return + } + etchJson, _ := json.Marshal(etching) + p.Printf("Etching:%s, data:%x", string(etchJson), data) + commitment := etching.Rune.Commitment() + btcConnector := NewMempoolConnector(config) + prvKey, address, _ := config.GetPrivateKeyAddr() + utxos, err := btcConnector.GetUtxos(address) + + cTx, rTx, err := BuildRuneEtchingTxs(prvKey, utxos, data, commitment, config.GetFeePerByte(), config.GetUtxoAmount(), config.GetNetwork(), address) + if err != nil { + p.Println("BuildRuneEtchingTxs error:", err.Error()) + return + } + p.Printf("commit Tx: %x\n", cTx) + p.Printf("reveal Tx: %x\n", rTx) + items := []string{i18n("SendTx"), i18n("WriteTxToFile")} + prompt := promptui.Select{ + Label: i18n("How to process the transaction?"), + Items: items, + } + + optionIdx, _, err := prompt.Run() + + if err != nil { + p.Printf("Prompt failed %v", err) + return + } + if optionIdx == 0 { //Direct send + SendTx(btcConnector, cTx, rTx) + } + if optionIdx == 1 { //write to file + WriteFile(string(etchJson), cTx, rTx) + } } -func testEtching() { - runeName := "STUDYZY.GMAIL.COM" - symbol := '曾' - myRune, err := runestone.SpacedRuneFromString(runeName) + +func SendTx(connector *MempoolConnector, ctx []byte, rtx []byte) { + tx := wire.NewMsgTx(wire.TxVersion) + tx.Deserialize(bytes.NewReader(ctx)) + ctxHash, err := connector.SendRawTransaction(tx, false) if err != nil { - fmt.Println(err) + p.Println("SendRawTransaction error:", err.Error()) return } - amt := uint128.From64(666666) - ca := uint128.From64(21000000) - etching := &runestone.Etching{ - Rune: &myRune.Rune, - Spacers: &myRune.Spacers, - Symbol: &symbol, - Terms: &runestone.Terms{ - Amount: &amt, - Cap: &ca, - }, - } - r := runestone.Runestone{Etching: etching} - data, err := r.Encipher() + p.Println("committed tx hash:", ctxHash) + if rtx == nil { + + return + } + p.Println("waiting for confirmations..., please don't close the program.") + //wail ctx tx confirm + lock.Lock() + go func(ctxHash *chainhash.Hash) { + for { + time.Sleep(30 * time.Second) + txInfo, err := connector.GetTxByHash(ctxHash.String()) + if err != nil { + p.Println("GetTransaction error:", err.Error()) + continue + } + p.Println("commit tx confirmations:", txInfo.Confirmations) + if txInfo.Confirmations > runestone.COMMIT_CONFIRMATIONS { + break + } + } + lock.Unlock() + }(ctxHash) + lock.Lock() //wait + tx = wire.NewMsgTx(wire.TxVersion) + tx.Deserialize(bytes.NewReader(rtx)) + rtxHash, err := connector.SendRawTransaction(tx, false) if err != nil { - fmt.Println(err) + p.Println("SendRawTransaction error:", err.Error()) + return } - fmt.Printf("Etching data: 0x%x\n", data) - dataString, _ := txscript.DisasmString(data) - fmt.Printf("Etching Script: %s\n", dataString) + p.Println("Etch complete, reveal tx hash:", rtxHash) } -func testMint() { - runeIdStr := "2609649:946" - runeId, _ := runestone.RuneIdFromString(runeIdStr) - r := runestone.Runestone{Mint: runeId} - data, err := r.Encipher() + +var lock sync.Mutex + +func WriteFile(etching string, tx []byte, tx2 []byte) { + //write to file + file, err := os.OpenFile("tx.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - fmt.Println(err) + p.Println("create file tx.txt error:", err.Error()) + return } - fmt.Printf("Mint Rune[%s] data: 0x%x\n", runeIdStr, data) - dataString, _ := txscript.DisasmString(data) - fmt.Printf("Mint Script: %s\n", dataString) + defer file.Close() + file.WriteString(time.Now().String()) + file.WriteString("Etching: " + etching) + file.WriteString("\n") + file.WriteString("Commit Tx: " + p.Sprintf("%x", tx)) + file.WriteString("\n") + if tx2 != nil { + file.WriteString("Reveal Tx: " + p.Sprintf("%x", tx2)) + file.WriteString("\n") + } + p.Println("write to file tx.txt") } -func testDecode() { - data, _ := hex.DecodeString("140114001600") //Mint UNCOMMON•GOODS - var tx wire.MsgTx - builder := txscript.NewScriptBuilder() - // Push opcode OP_RETURN - builder.AddOp(txscript.OP_RETURN) - // Push MAGIC_NUMBER - builder.AddOp(runestone.MAGIC_NUMBER) - // Push payload - builder.AddData(data) - pkScript, _ := builder.Script() - txOut := wire.NewTxOut(0, pkScript) - tx.AddTxOut(txOut) - r := &runestone.Runestone{} - artifact, err := r.Decipher(&tx) + +func BuildMintTxs() { + runeId, err := config.GetMint() + if err != nil { + p.Println(err.Error()) + return + + } + r := runestone.Runestone{Mint: runeId} + runeData, err := r.Encipher() + if err != nil { + p.Println(err) + } + p.Printf("Mint Rune[%s] data: 0x%x\n", config.Mint.RuneId, runeData) + //dataString, _ := txscript.DisasmString(data) + //p.Printf("Mint Script: %s\n", dataString) + btcConnector := NewMempoolConnector(config) + prvKey, address, _ := config.GetPrivateKeyAddr() + utxos, err := btcConnector.GetUtxos(address) + tx, err := BuildTransferBTCTx(prvKey, utxos, address, config.GetUtxoAmount(), config.GetFeePerByte(), config.GetNetwork(), runeData) if err != nil { - fmt.Println(err) + p.Println("BuildMintRuneTx error:", err.Error()) return } - a, _ := json.Marshal(artifact) - fmt.Printf("Artifact: %s\n", string(a)) + p.Printf("mint rune tx: %x\n", tx) + items := []string{i18n("SendTx"), i18n("WriteTxToFile")} + prompt := promptui.Select{ + Label: i18n("How to process the transaction?"), + Items: items, + } + + optionIdx, _, err := prompt.Run() + + if err != nil { + p.Printf("Prompt failed %v", err) + return + } + if optionIdx == 0 { //Direct send + SendTx(btcConnector, tx, nil) + } + if optionIdx == 1 { //write to file + WriteFile(p.Sprintf("Mint rune[%s]", runeId.String()), tx, nil) + } } diff --git a/cmd/runestonecli/mempool.go b/cmd/runestonecli/mempool.go new file mode 100644 index 0000000..654d822 --- /dev/null +++ b/cmd/runestonecli/mempool.go @@ -0,0 +1,305 @@ +package main + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/pkg/errors" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +type MempoolConnector struct { + baseUrl string + network *chaincfg.Params +} + +func NewMempoolConnector(config Config) *MempoolConnector { + baseURL := config.RpcUrl + //net := config.Network + //if net == "mainet" { + // baseURL = "https://mempool.space/api" + //} else if net == "testnet" { + // //baseURL = "https://mempool.space/testnet/api" + // baseURL = "https://blockstream.info/testnet/api" + //} else if net == "signet" { + // baseURL = "https://mempool.space/signet/api" + //} else { + // log.Fatal("mempool don't support other netParams") + //} + connector := &MempoolConnector{ + baseUrl: baseURL, + network: config.GetNetwork(), + } + return connector +} + +func (m MempoolConnector) GetBlockHeight() (uint64, error) { + res, err := m.request(http.MethodGet, "/blocks/tip/height", nil) + if err != nil { + return 0, err + } + var height uint64 + err = json.Unmarshal(res, &height) + if err != nil { + return 0, err + } + log.Printf("found latest block height %d", height) + return height, nil + +} + +func (m MempoolConnector) GetBlockHashByHeight(height uint64) ([]byte, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/block-height/%d", height), nil) + if err != nil { + return nil, err + } + hashString := string(res) + hash, err := hex.DecodeString(hashString) + log.Printf("found block hash %x by height:%d", hash, height) + return hash, nil +} + +func (m MempoolConnector) GetBlockByHash(blockHash Hash) (*wire.MsgBlock, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/block/%s/raw", blockHash), nil) + if err != nil { + return nil, err + } + //unmarshal the response + block := &wire.MsgBlock{} + if err := block.Deserialize(bytes.NewReader(res)); err != nil { + return nil, err + } + log.Printf("found block %s", blockHash) + return block, nil +} + +func (m MempoolConnector) GetHeaderByHash(h Hash) (*wire.BlockHeader, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/block/%s/header", h), nil) + if err != nil { + return nil, err + } + headerBytes, err := hex.DecodeString(string(res)) + if err != nil { + return nil, err + } + //unmarshal the response + header := &wire.BlockHeader{} + if err := header.Deserialize(bytes.NewReader(headerBytes)); err != nil { + return nil, err + } + log.Printf("found header %s", h) + return header, nil +} + +func (m MempoolConnector) GetBlockByHeight(height uint64) (*wire.MsgBlock, error) { + hash, err := m.GetBlockHashByHeight(height) + if err != nil { + return nil, err + } + return m.GetBlockByHash(Hash(hash)) +} + +func (m MempoolConnector) GetBlockTxIDS(bh Hash) ([]Hash, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/block/%s/txids", bh), nil) + if err != nil { + return nil, err + } + //unmarshal the response + var txids []string + err = json.Unmarshal(res, &txids) + if err != nil { + return nil, err + } + hashes := make([]Hash, len(txids)) + for i, txid := range txids { + hashes[i] = HexToHash(txid) + } + log.Printf("found %d txids for block %s", len(hashes), bh) + return hashes, nil +} +func (m MempoolConnector) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + log.Printf("send tx %s to bitcoin network", tx.TxHash()) + //th := tx.TxHash() + //return &th, nil + + var buf bytes.Buffer + if err := tx.Serialize(&buf); err != nil { + return nil, err + } + + res, err := m.request(http.MethodPost, "/tx", strings.NewReader(hex.EncodeToString(buf.Bytes()))) + if err != nil { + return nil, err + } + + txHash, err := chainhash.NewHashFromStr(string(res)) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to parse tx hash, %s", string(res))) + } + return txHash, nil +} + +func (m MempoolConnector) GetUtxos(address string) ([]*Utxo, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/address/%s/utxo", address), nil) + if err != nil { + return nil, err + } + //unmarshal the response + var mutxos []mempoolUTXO + err = json.Unmarshal(res, &mutxos) + if err != nil { + return nil, err + } + addr, err := btcutil.DecodeAddress(address, m.network) + if err != nil { + return nil, err + } + pkScript, _ := txscript.PayToAddrScript(addr) + utxos := make([]*Utxo, len(mutxos)) + for i, mutxo := range mutxos { + txHash, err := chainhash.NewHashFromStr(mutxo.Txid) + if err != nil { + return nil, err + } + utxos[i] = &Utxo{ + TxHash: BytesToHash(txHash.CloneBytes()), + Index: uint32(mutxo.Vout), + Value: mutxo.Value, + PkScript: pkScript, + } + } + log.Printf("found %d unspent outputs for address %s", len(utxos), address) + return utxos, nil +} + +type txStatus struct { + Confirmed bool `json:"confirmed"` + BlockHeight uint64 `json:"block_height"` + BlockHash string `json:"block_hash"` + BlockTime int64 `json:"block_time"` +} +type txResponse struct { + Txid string `json:"txid"` + Version int `json:"version"` + Locktime int `json:"locktime"` + Size int `json:"size"` + Fee int `json:"fee"` + Status txStatus `json:"status"` +} + +func (m MempoolConnector) GetTxByHash(hash string) (*BtcTxInfo, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/tx/%s", hash), nil) + if err != nil { + return nil, err + } + //unmarshal the response + var resp txResponse + err = json.Unmarshal(res, &resp) + if err != nil { + return nil, err + } + txInfo := &BtcTxInfo{ + Tx: nil, + BlockHeight: resp.Status.BlockHeight, + BlockHash: HexToHash(resp.Status.BlockHash), + BlockTime: uint64(resp.Status.BlockTime), + Confirmations: 0, + TxIndex: 0, + } + tx, err := m.GetRawTxByHash(hash) + if err != nil { + return nil, err + } + txInfo.Tx = tx + return txInfo, nil +} + +func (m MempoolConnector) GetRawTxByHash(hash string) (*wire.MsgTx, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/tx/%s/raw", hash), nil) + if err != nil { + return nil, err + } + //unmarshal the response + tx := &wire.MsgTx{} + if err := tx.Deserialize(bytes.NewReader(res)); err != nil { + return nil, err + } + log.Printf("found tx %s", hash) + return tx, nil +} + +type mempoolUTXO struct { + Txid string `json:"txid"` + Vout int `json:"vout"` + Status struct { + Confirmed bool `json:"confirmed"` + BlockHeight int `json:"block_height"` + BlockHash string `json:"block_hash"` + BlockTime int64 `json:"block_time"` + } `json:"status"` + Value int64 `json:"value"` +} + +func (m MempoolConnector) request(method, subPath string, requestBody io.Reader) ([]byte, error) { + url := fmt.Sprintf("%s%s", m.baseUrl, subPath) + + req, err := http.NewRequest(method, url, requestBody) + if err != nil { + return nil, errors.Wrap(err, "failed to create request") + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to send request") + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read response body") + } + + return body, nil +} + +func (m MempoolConnector) GetBalance(address string) (uint64, error) { + res, err := m.request(http.MethodGet, fmt.Sprintf("/address/%s", address), nil) + if err != nil { + return 0, err + } + //unmarshal the response + var balance struct { + ChainStats struct { + FundedTxoCount uint64 `json:"funded_txo_count"` + FundedTxoSum uint64 `json:"funded_txo_sum"` + SpentTxoCount uint64 `json:"spent_txo_count"` + SpentTxoSum uint64 `json:"spent_txo_sum"` + } `json:"chain_stats"` + } + err = json.Unmarshal(res, &balance) + if err != nil { + return 0, err + } + log.Printf("found balance %d for address %s", balance.ChainStats.FundedTxoSum-balance.ChainStats.SpentTxoSum, address) + return balance.ChainStats.FundedTxoSum - balance.ChainStats.SpentTxoSum, nil +} + +type BtcTxInfo struct { + Tx *wire.MsgTx + BlockHeight uint64 + BlockHash Hash + BlockTime uint64 + Confirmations uint64 + TxIndex uint64 +} diff --git a/cmd/runestonecli/ordi-tx.go b/cmd/runestonecli/ordi-tx.go new file mode 100644 index 0000000..c3aaab0 --- /dev/null +++ b/cmd/runestonecli/ordi-tx.go @@ -0,0 +1,351 @@ +package main + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "sort" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/mempool" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +const ( + defaultSequenceNum = wire.MaxTxInSequenceNum - 10 + defaultRevealOutValue = int64(330) // 500 sat, ord default 10000 + + MaxStandardTxWeight = blockchain.MaxBlockWeight / 10 +) + +func BuildInscriptionTxs(privateKey *btcec.PrivateKey, utxo []*Utxo, mime string, content []byte, feeRate int64, revealValue int64, net *chaincfg.Params) ([]byte, []byte, error) { + //build 2 tx, 1 transfer BTC to taproot address, 2 inscription transfer taproot address to another address + pubKey := privateKey.PubKey() + receiver, err := getP2TRAddress(pubKey, net) + if err != nil { + return nil, nil, err + } + // 1. build inscription script + inscriptionScript, err := CreateInscriptionScript(pubKey, mime, content) + if err != nil { + return nil, nil, err + } + inscriptionAddress, err := GetTapScriptAddress(pubKey, inscriptionScript, net) + if err != nil { + return nil, nil, err + } + inscriptionPkScript, _ := txscript.PayToAddrScript(inscriptionAddress) + // 2. build reveal tx + revealTx, totalPrevOutput, err := buildEmptyRevealTx(receiver, inscriptionScript, revealValue, feeRate, nil) + if err != nil { + return nil, nil, err + } + // 3. build commit tx + out := &wire.TxOut{ + Value: totalPrevOutput, + PkScript: inscriptionPkScript, + } + commitTx, err := buildCommitTx(utxo, out, feeRate, nil, true) + if err != nil { + return nil, nil, err + } + // 4. completeRevealTx + revealTx, err = completeRevealTx(privateKey, commitTx, revealTx, inscriptionScript) + if err != nil { + return nil, nil, err + } + // 5. sign commit tx + commitTx, err = signCommitTx(privateKey, utxo, commitTx) + if err != nil { + return nil, nil, err + } + // 6. serialize + commitTxBytes, err := serializeTx(commitTx) + if err != nil { + return nil, nil, err + } + revealTxBytes, err := serializeTx(revealTx) + if err != nil { + return nil, nil, err + } + return commitTxBytes, revealTxBytes, nil +} +func BuildRuneEtchingTxs(privateKey *btcec.PrivateKey, utxo []*Utxo, runeOpReturnData []byte, runeCommitment []byte, + feeRate int64, revealValue int64, net *chaincfg.Params, toAddr string) ([]byte, []byte, error) { + //build 2 tx, 1 transfer BTC to taproot address, 2 inscription transfer taproot address to another address + pubKey := privateKey.PubKey() + receiver, err := btcutil.DecodeAddress(toAddr, net) + if err != nil { + return nil, nil, err + } + // 1. build inscription script + inscriptionScript, err := CreateCommitmentScript(pubKey, runeCommitment) + if err != nil { + return nil, nil, err + } + inscriptionAddress, err := GetTapScriptAddress(pubKey, inscriptionScript, net) + if err != nil { + return nil, nil, err + } + inscriptionPkScript, _ := txscript.PayToAddrScript(inscriptionAddress) + // 2. build reveal tx + revealTx, totalPrevOutput, err := buildEmptyRevealTx(receiver, inscriptionScript, revealValue, feeRate, runeOpReturnData) + if err != nil { + return nil, nil, err + } + // 3. build commit tx + out := &wire.TxOut{ + Value: totalPrevOutput, + PkScript: inscriptionPkScript, + } + commitTx, err := buildCommitTx(utxo, out, feeRate, nil, true) + if err != nil { + return nil, nil, err + } + // 4. completeRevealTx + revealTx, err = completeRevealTx(privateKey, commitTx, revealTx, inscriptionScript) + if err != nil { + return nil, nil, err + } + // 5. sign commit tx + commitTx, err = signCommitTx(privateKey, utxo, commitTx) + if err != nil { + return nil, nil, err + } + // 6. serialize + commitTxBytes, err := serializeTx(commitTx) + if err != nil { + return nil, nil, err + } + revealTxBytes, err := serializeTx(revealTx) + if err != nil { + return nil, nil, err + } + return commitTxBytes, revealTxBytes, nil +} +func BuildTransferBTCTx(privateKey *btcec.PrivateKey, utxo []*Utxo, toAddr string, toAmount, feeRate int64, net *chaincfg.Params, runeData []byte) ([]byte, error) { + address, err := btcutil.DecodeAddress(toAddr, net) + if err != nil { + return nil, err + } + pkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + // 1. build tx + transferTx, err := buildCommitTx(utxo, wire.NewTxOut(toAmount, pkScript), feeRate, runeData, true) + if err != nil { + return nil, err + } + // 2.sign tx + transferTx, err = signCommitTx(privateKey, utxo, transferTx) + if err != nil { + return nil, err + } + // 3. serialize + commitTxBytes, err := serializeTx(transferTx) + if err != nil { + return nil, err + } + return commitTxBytes, nil +} + +func VerifyTx(rawTx string, prevTxOutScript []byte, prevTxOutValue int64) error { + txBytes, err := hex.DecodeString(rawTx) + if err != nil { + fmt.Println("Error decoding transaction:", err) + return err + } + + var tx wire.MsgTx + if err := tx.Deserialize(bytes.NewReader(txBytes)); err != nil { + fmt.Println("Error deserializing transaction:", err) + return err + } + + for i, _ := range tx.TxIn { + + outputFetcher := txscript.NewCannedPrevOutputFetcher(prevTxOutScript, prevTxOutValue) + sigHashes := txscript.NewTxSigHashes(&tx, outputFetcher) + vm, err := txscript.NewEngine(prevTxOutScript, &tx, i, txscript.StandardVerifyFlags, nil, sigHashes, prevTxOutValue, outputFetcher) + if err != nil { + fmt.Printf("Error creating script engine for input %d: %v\n", i, err) + return err + } + + if err := vm.Execute(); err != nil { + fmt.Printf("Invalid signature for input %d: %v\n", i, err) + return err + } else { + fmt.Printf("Valid signature for input %d\n", i) + } + } + fmt.Println("Transaction successfully verified") + return nil +} + +func buildEmptyRevealTx(receiver btcutil.Address, inscriptionScript []byte, revealOutValue, feeRate int64, opReturnData []byte) ( + *wire.MsgTx, int64, error) { + totalPrevOutput := int64(0) + tx := wire.NewMsgTx(wire.TxVersion) + // add 1 txin + in := wire.NewTxIn(&wire.OutPoint{Index: uint32(0)}, nil, nil) + in.Sequence = defaultSequenceNum + tx.AddTxIn(in) + if len(opReturnData) > 0 { + tx.AddTxOut(wire.NewTxOut(0, opReturnData)) + } + // add 1 txout + scriptPubKey, err := txscript.PayToAddrScript(receiver) + if err != nil { + return nil, 0, err + } + out := wire.NewTxOut(revealOutValue, scriptPubKey) + tx.AddTxOut(out) + // calculate total prev output + revealBaseTxFee := int64(tx.SerializeSize()) * feeRate + totalPrevOutput += revealOutValue + revealBaseTxFee + // add witness + emptySignature := make([]byte, 64) + emptyControlBlockWitness := make([]byte, 33) + // calculate total prev output + fee := (int64(wire.TxWitness{emptySignature, inscriptionScript, emptyControlBlockWitness}.SerializeSize()+2+3) / 4) * feeRate + totalPrevOutput += fee + + return tx, totalPrevOutput, nil +} +func findBestUtxo(commitTxOutPointList []*Utxo, totalRevealPrevOutput, commitFeeRate int64) []*Utxo { + sort.Slice(commitTxOutPointList, func(i, j int) bool { + return commitTxOutPointList[i].Value > commitTxOutPointList[j].Value + }) + best := make([]*Utxo, 0) + total := int64(0) + for _, utxo := range commitTxOutPointList { + if total >= totalRevealPrevOutput+commitFeeRate { + break + } + best = append(best, utxo) + total += utxo.Value + } + return best +} + +func buildCommitTx(commitTxOutPointList []*Utxo, revealTxPrevOutput *wire.TxOut, commitFeeRate int64, runeData []byte, splitChangeOutput bool) (*wire.MsgTx, error) { + totalSenderAmount := btcutil.Amount(0) + totalRevealPrevOutput := revealTxPrevOutput.Value + tx := wire.NewMsgTx(wire.TxVersion) + var changePkScript *[]byte + bestUtxo := findBestUtxo(commitTxOutPointList, totalRevealPrevOutput, commitFeeRate) + for _, utxo := range bestUtxo { + txOut := utxo.TxOut() + outPoint := utxo.OutPoint() + if changePkScript == nil { // first sender as change address + changePkScript = &txOut.PkScript + } + in := wire.NewTxIn(&outPoint, nil, nil) + in.Sequence = defaultSequenceNum + tx.AddTxIn(in) + totalSenderAmount += btcutil.Amount(txOut.Value) + } + if len(runeData) > 0 { + + tx.AddTxOut(wire.NewTxOut(0, runeData)) + + } + // add reveal tx output + tx.AddTxOut(revealTxPrevOutput) + if splitChangeOutput || !bytes.Equal(*changePkScript, revealTxPrevOutput.PkScript) { + // add change output + tx.AddTxOut(wire.NewTxOut(0, *changePkScript)) + } + //mock witness to calculate fee + emptySignature := make([]byte, 64) + for _, in := range tx.TxIn { + in.Witness = wire.TxWitness{emptySignature} + } + fee := btcutil.Amount(mempool.GetTxVirtualSize(btcutil.NewTx(tx))) * btcutil.Amount(commitFeeRate) + changeAmount := totalSenderAmount - btcutil.Amount(totalRevealPrevOutput) - fee + if changeAmount > 0 { + tx.TxOut[len(tx.TxOut)-1].Value += int64(changeAmount) + } else { + tx.TxOut = tx.TxOut[:len(tx.TxOut)-1] + if changeAmount < 0 { + feeWithoutChange := btcutil.Amount(mempool.GetTxVirtualSize(btcutil.NewTx(tx))) * btcutil.Amount(commitFeeRate) + if totalSenderAmount-btcutil.Amount(totalRevealPrevOutput)-feeWithoutChange < 0 { + return nil, errors.New("insufficient balance") + } + } + } + //clear mock witness + for _, in := range tx.TxIn { + in.Witness = nil + } + return tx, nil +} + +func completeRevealTx(privateKey *btcec.PrivateKey, commitTx *wire.MsgTx, revealTx *wire.MsgTx, inscriptionScript []byte) (*wire.MsgTx, error) { + //set commit tx hash to reveal tx input + revealTx.TxIn[0].PreviousOutPoint.Hash = commitTx.TxHash() + // witness[0]. sign commit tx + revealTxPrevOutputFetcher := txscript.NewCannedPrevOutputFetcher(commitTx.TxOut[0].PkScript, commitTx.TxOut[0].Value) + tsHash, err := txscript.CalcTapscriptSignaturehash(txscript.NewTxSigHashes(revealTx, revealTxPrevOutputFetcher), + txscript.SigHashDefault, revealTx, 0, revealTxPrevOutputFetcher, txscript.NewBaseTapLeaf(inscriptionScript)) + if err != nil { + return nil, err + } + signature, err := schnorr.Sign(privateKey, tsHash) + if err != nil { + return nil, err + } + //witness[2]. build control block + leafNode := txscript.NewBaseTapLeaf(inscriptionScript) + proof := &txscript.TapscriptProof{ + TapLeaf: leafNode, + RootNode: leafNode, + } + controlBlock := proof.ToControlBlock(privateKey.PubKey()) + controlBlockWitness, err := controlBlock.ToBytes() + if err != nil { + return nil, err + } + // 3. set full witness + revealTx.TxIn[0].Witness = wire.TxWitness{signature.Serialize(), inscriptionScript, controlBlockWitness} + + // check tx max tx weight + + revealWeight := blockchain.GetTransactionWeight(btcutil.NewTx(revealTx)) + if revealWeight > MaxStandardTxWeight { + return nil, errors.New(fmt.Sprintf("reveal(index %d) transaction weight greater than %d (MAX_STANDARD_TX_WEIGHT): %d", 0, MaxStandardTxWeight, revealWeight)) + } + + return revealTx, nil +} + +func signCommitTx(prvKey *btcec.PrivateKey, utxos []*Utxo, commitTx *wire.MsgTx) (*wire.MsgTx, error) { + // build utxoList for FetchPrevOutput + utxoList := UtxoList(utxos) + for i, txIn := range commitTx.TxIn { + txOut := utxoList.FetchPrevOutput(commitTx.TxIn[i].PreviousOutPoint) + witness, err := txscript.TaprootWitnessSignature(commitTx, txscript.NewTxSigHashes(commitTx, utxoList), + i, txOut.Value, txOut.PkScript, txscript.SigHashDefault, prvKey) + if err != nil { + return nil, err + } + txIn.Witness = witness + } + + return commitTx, nil +} +func serializeTx(tx *wire.MsgTx) ([]byte, error) { + var buf bytes.Buffer + if err := tx.Serialize(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/cmd/runestonecli/tapescript.go b/cmd/runestonecli/tapescript.go new file mode 100644 index 0000000..9a3f419 --- /dev/null +++ b/cmd/runestonecli/tapescript.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "encoding/hex" + "errors" + "log" + "strings" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +func CreateInscriptionScript(pk *btcec.PublicKey, contentType string, fileBytes []byte) ([]byte, error) { + builder := txscript.NewScriptBuilder() + //push pubkey + pk32 := schnorr.SerializePubKey(pk) + log.Printf("put pubkey:%x to tapScript", pk32) + builder.AddData(pk32) + builder.AddOp(txscript.OP_CHECKSIG) + //Ordinals script + builder.AddOp(txscript.OP_FALSE) + builder.AddOp(txscript.OP_IF) + builder.AddData([]byte("ord")) + builder.AddOp(txscript.OP_DATA_1) //?? + builder.AddOp(txscript.OP_DATA_1) //?? + builder.AddData([]byte(contentType)) + builder.AddOp(txscript.OP_0) + data, err := builder.Script() + if err != nil { + return nil, err + } + splitLen := 520 + point := 0 + for { + builder = txscript.NewScriptBuilder() + if point+splitLen > len(fileBytes) { + builder.AddData(fileBytes[point:]) + data1, err1 := builder.Script() + if err1 != nil { + return nil, err1 + } + data = append(data, data1...) + break + } + builder.AddData(fileBytes[point : point+splitLen]) + data1, err1 := builder.Script() + if err1 != nil { + return nil, err1 + } + data = append(data, data1...) + point += splitLen + } + data = append(data, txscript.OP_ENDIF) + return data, err +} + +func CreateCommitmentScript(pk *btcec.PublicKey, commitment []byte) ([]byte, error) { + builder := txscript.NewScriptBuilder() + //push pubkey + pk32 := schnorr.SerializePubKey(pk) + + builder.AddData(pk32) + builder.AddOp(txscript.OP_CHECKSIG) + //Commitment script + builder.AddOp(txscript.OP_FALSE) + builder.AddOp(txscript.OP_IF) + builder.AddData(commitment) + builder.AddOp(txscript.OP_ENDIF) + return builder.Script() +} +func IsTapScript(witness wire.TxWitness) bool { + if len(witness) != 3 { + return false + } + witness2 := witness[2] + if len(witness2) == 33 && (witness2[0] == 0xc0 || witness2[0] == 0xc1) { + return true + } + return false +} + +func GetOrdinalsContent(tapScript []byte) (mime string, content []byte, err error) { + scriptStr, err := txscript.DisasmString(tapScript) + if err != nil { + return "", nil, err + } + start := false + scriptStrArray := strings.Split(scriptStr, " ") + contentHex := "" + for i := 0; i < len(scriptStrArray); i++ { + if scriptStrArray[i] == "6f7264" { // 6f7264 ==ord + start = true + mimeBytes, _ := hex.DecodeString(scriptStrArray[i+2]) + mime = string(mimeBytes) + i = i + 4 + } + if i < len(scriptStrArray) { + if start { + contentHex = contentHex + scriptStrArray[i] + } + if scriptStrArray[i] == "OP_ENDIF" { + break + } + } + } + contentBytes, _ := hex.DecodeString(contentHex) + return mime, contentBytes, nil +} + +var ordiBytes []byte + +func init() { + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_FALSE) + builder.AddOp(txscript.OP_IF) + builder.AddData([]byte("ord")) + builder.AddOp(txscript.OP_DATA_1) + ordiBytes, _ = builder.Script() +} +func IsOrdinalsScript(script []byte) bool { + if bytes.Contains(script, ordiBytes) && script[len(script)-1] == txscript.OP_ENDIF { + return true + } + return false +} + +func GetInscriptionContent(tx *wire.MsgTx) (contentType string, content []byte, err error) { + for _, txIn := range tx.TxIn { + if IsTapScript(txIn.Witness) { + if IsOrdinalsScript(txIn.Witness[1]) { + contentType, data, err := GetOrdinalsContent(txIn.Witness[1]) + if err != nil { + return "", nil, err + } + return contentType, data, nil + } + } + } + return "", nil, errors.New("no ordinals script found") +} diff --git a/cmd/runestonecli/utxo.go b/cmd/runestonecli/utxo.go new file mode 100644 index 0000000..d0601f5 --- /dev/null +++ b/cmd/runestonecli/utxo.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "fmt" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +type Utxo struct { + TxHash Hash + Index uint32 + Value int64 + PkScript []byte +} + +func (u *Utxo) OutPoint() wire.OutPoint { + h, _ := chainhash.NewHash(u.TxHash[:]) + return wire.OutPoint{ + Hash: *h, + Index: u.Index, + } +} +func (u *Utxo) TxOut() *wire.TxOut { + return wire.NewTxOut(u.Value, u.PkScript) +} + +type UtxoList []*Utxo + +func (l UtxoList) Add(utxo *Utxo) UtxoList { + return append(l, utxo) +} +func (l UtxoList) FetchPrevOutput(o wire.OutPoint) *wire.TxOut { + for _, utxo := range l { + if bytes.Equal(utxo.TxHash[:], o.Hash[:]) && utxo.Index == o.Index { + return wire.NewTxOut(utxo.Value, utxo.PkScript) + } + } + return nil +} +func (u *Utxo) String() string { + return fmt.Sprintf("TxHash: %s, Index: %d, Value: %d, PkScript: %x", u.TxHash, u.Index, u.Value, u.PkScript) +}