├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── ord └── ord.go └── pkg ├── btcapi ├── btcapi.go └── mempool │ ├── addresses.go │ ├── addresses_test.go │ ├── client.go │ ├── transactions.go │ └── transactions_test.go └── rpcclient ├── import_descriptors_cmds.go └── import_descriptors_cmds_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,goland+all,intellij+all,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,goland+all,intellij+all,visualstudiocode 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### GoLand+all ### 28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 30 | 31 | # User-specific stuff 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | .idea/**/usage.statistics.xml 35 | .idea/**/dictionaries 36 | .idea/**/shelf 37 | 38 | # AWS User-specific 39 | .idea/**/aws.xml 40 | 41 | # Generated files 42 | .idea/**/contentModel.xml 43 | 44 | # Sensitive or high-churn files 45 | .idea/**/dataSources/ 46 | .idea/**/dataSources.ids 47 | .idea/**/dataSources.local.xml 48 | .idea/**/sqlDataSources.xml 49 | .idea/**/dynamic.xml 50 | .idea/**/uiDesigner.xml 51 | .idea/**/dbnavigator.xml 52 | 53 | # Gradle 54 | .idea/**/gradle.xml 55 | .idea/**/libraries 56 | 57 | # Gradle and Maven with auto-import 58 | # When using Gradle or Maven with auto-import, you should exclude module files, 59 | # since they will be recreated, and may cause churn. Uncomment if using 60 | # auto-import. 61 | # .idea/artifacts 62 | # .idea/compiler.xml 63 | # .idea/jarRepositories.xml 64 | # .idea/modules.xml 65 | # .idea/*.iml 66 | # .idea/modules 67 | # *.iml 68 | # *.ipr 69 | 70 | # CMake 71 | cmake-build-*/ 72 | 73 | # Mongo Explorer plugin 74 | .idea/**/mongoSettings.xml 75 | 76 | # File-based project format 77 | *.iws 78 | 79 | # IntelliJ 80 | out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # SonarLint plugin 92 | .idea/sonarlint/ 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | # Editor-based Rest Client 101 | .idea/httpRequests 102 | 103 | # Android studio 3.1+ serialized cache file 104 | .idea/caches/build_file_checksums.ser 105 | 106 | ### GoLand+all Patch ### 107 | # Ignore everything but code style settings and run configurations 108 | # that are supposed to be shared within teams. 109 | 110 | .idea/* 111 | 112 | !.idea/codeStyles 113 | !.idea/runConfigurations 114 | 115 | ### Intellij+all ### 116 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 117 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 118 | 119 | # User-specific stuff 120 | 121 | # AWS User-specific 122 | 123 | # Generated files 124 | 125 | # Sensitive or high-churn files 126 | 127 | # Gradle 128 | 129 | # Gradle and Maven with auto-import 130 | # When using Gradle or Maven with auto-import, you should exclude module files, 131 | # since they will be recreated, and may cause churn. Uncomment if using 132 | # auto-import. 133 | # .idea/artifacts 134 | # .idea/compiler.xml 135 | # .idea/jarRepositories.xml 136 | # .idea/modules.xml 137 | # .idea/*.iml 138 | # .idea/modules 139 | # *.iml 140 | # *.ipr 141 | 142 | # CMake 143 | 144 | # Mongo Explorer plugin 145 | 146 | # File-based project format 147 | 148 | # IntelliJ 149 | 150 | # mpeltonen/sbt-idea plugin 151 | 152 | # JIRA plugin 153 | 154 | # Cursive Clojure plugin 155 | 156 | # SonarLint plugin 157 | 158 | # Crashlytics plugin (for Android Studio and IntelliJ) 159 | 160 | # Editor-based Rest Client 161 | 162 | # Android studio 3.1+ serialized cache file 163 | 164 | ### Intellij+all Patch ### 165 | # Ignore everything but code style settings and run configurations 166 | # that are supposed to be shared within teams. 167 | 168 | 169 | 170 | ### VisualStudioCode ### 171 | .vscode/* 172 | !.vscode/settings.json 173 | !.vscode/tasks.json 174 | !.vscode/launch.json 175 | !.vscode/extensions.json 176 | !.vscode/*.code-snippets 177 | 178 | # Local History for Visual Studio Code 179 | .history/ 180 | 181 | # Built Visual Studio Code Extensions 182 | *.vsix 183 | 184 | ### VisualStudioCode Patch ### 185 | # Ignore all local history of files 186 | .history 187 | .ionide 188 | 189 | # End of https://www.toptal.com/developers/gitignore/api/go,goland+all,intellij+all,visualstudiocode 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ord-tx 2 | 3 | ## golang ordinals btc nft inscribe tx 4 | 5 | ### Supports two types of batch inscriptions: 6 | 7 | - One commit transaction with multiple reveal transactions. 8 | - One commit transaction with a single reveal transaction. 9 | 10 | ## [Inscription Example](https://github.com/VincentDebug/go-ord-tx-example) 11 | 12 | The inscribewithoutnoderpc, inscribewithprivatenoderpc, and inscribewithpublicnoderpc examples demonstrate how to use the go-ord-tx package to create inscriptions. 13 | 14 | - inscribewithoutnoderpc: sends raw transactions directly to the network, without using an RPC node; 15 | - inscribewithprivatenoderpc: uses a private RPC node. 16 | - inscribewithpublicnoderpc: uses a public RPC node. 17 | - more see [Inscription Example](https://github.com/VincentDebug/go-ord-tx-example) 18 | 19 | ## Contributing 20 | 21 | We welcome contributions to this project. If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome. 22 | 23 | ## Acknowledgments 24 | 25 | We would like to thank the following contributors: 26 | 27 | [![laizy](https://github.com/laizy.png?size=50)](https://github.com/laizy) 28 | 29 | Your contributions to this project are greatly appreciated. 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vincentdebug/go-ord-tx 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/btcsuite/btcd v0.23.4 7 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 8 | github.com/btcsuite/btcd/btcutil v1.1.3 9 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 10 | github.com/pkg/errors v0.9.1 11 | ) 12 | 13 | require ( 14 | github.com/aead/siphash v1.0.1 // indirect 15 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 16 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 17 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 18 | github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 20 | github.com/kkdai/bstream v1.0.0 // indirect 21 | golang.org/x/crypto v0.16.0 // indirect 22 | golang.org/x/sys v0.15.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= 2 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 3 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 4 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 5 | github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= 6 | github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= 7 | github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= 8 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 9 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 10 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= 11 | github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 12 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 13 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 14 | github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= 15 | github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= 16 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 17 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 18 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= 19 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 20 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= 21 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 22 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 23 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= 24 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 25 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 26 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 27 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 28 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 29 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= 30 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 31 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 32 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 37 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 38 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 39 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 40 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 41 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 42 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 43 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 44 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 45 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 46 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 47 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 48 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 49 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 50 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 51 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 52 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 53 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 54 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 55 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 56 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 57 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 58 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 60 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 61 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 62 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 63 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= 64 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 65 | github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= 66 | github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= 67 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 68 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 69 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 70 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 71 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 72 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 73 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 74 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 75 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 76 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 77 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 83 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 84 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 85 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 86 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 87 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 88 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 89 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 90 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 91 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 92 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 93 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 95 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 96 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 97 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 98 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 99 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= 108 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 111 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 112 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 113 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 114 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 115 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 116 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 119 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 120 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 121 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 122 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 123 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 124 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 125 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 126 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 127 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 128 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 129 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 132 | -------------------------------------------------------------------------------- /ord/ord.go: -------------------------------------------------------------------------------- 1 | package ord 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "github.com/btcsuite/btcd/blockchain" 8 | "github.com/btcsuite/btcd/btcec/v2" 9 | "github.com/btcsuite/btcd/btcec/v2/schnorr" 10 | "github.com/btcsuite/btcd/btcjson" 11 | "github.com/btcsuite/btcd/btcutil" 12 | "github.com/btcsuite/btcd/chaincfg" 13 | "github.com/btcsuite/btcd/chaincfg/chainhash" 14 | "github.com/btcsuite/btcd/mempool" 15 | "github.com/btcsuite/btcd/rpcclient" 16 | "github.com/btcsuite/btcd/txscript" 17 | "github.com/btcsuite/btcd/wire" 18 | "github.com/pkg/errors" 19 | "github.com/vincentdebug/go-ord-tx/pkg/btcapi" 20 | extRpcClient "github.com/vincentdebug/go-ord-tx/pkg/rpcclient" 21 | "log" 22 | ) 23 | 24 | type InscriptionData struct { 25 | ContentType string 26 | Body []byte 27 | Destination string 28 | } 29 | 30 | type InscriptionRequest struct { 31 | CommitTxOutPointList []*wire.OutPoint 32 | CommitTxPrivateKeyList []*btcec.PrivateKey // If used without RPC, 33 | // a local signature is required for committing the commit tx. 34 | // Currently, CommitTxPrivateKeyList[i] sign CommitTxOutPointList[i] 35 | CommitFeeRate int64 36 | FeeRate int64 37 | DataList []InscriptionData 38 | SingleRevealTxOnly bool // Currently, the official Ordinal parser can only parse a single NFT per transaction. 39 | // When the official Ordinal parser supports parsing multiple NFTs in the future, we can consider using a single reveal transaction. 40 | RevealOutValue int64 41 | } 42 | 43 | type inscriptionTxCtxData struct { 44 | privateKey *btcec.PrivateKey 45 | inscriptionScript []byte 46 | commitTxAddressPkScript []byte 47 | controlBlockWitness []byte 48 | recoveryPrivateKeyWIF string 49 | revealTxPrevOutput *wire.TxOut 50 | } 51 | 52 | type blockchainClient struct { 53 | rpcClient *rpcclient.Client 54 | btcApiClient btcapi.BTCAPIClient 55 | } 56 | 57 | type InscriptionTool struct { 58 | net *chaincfg.Params 59 | client *blockchainClient 60 | commitTxPrevOutputFetcher *txscript.MultiPrevOutFetcher 61 | commitTxPrivateKeyList []*btcec.PrivateKey 62 | txCtxDataList []*inscriptionTxCtxData 63 | revealTxPrevOutputFetcher *txscript.MultiPrevOutFetcher 64 | revealTx []*wire.MsgTx 65 | commitTx *wire.MsgTx 66 | } 67 | 68 | const ( 69 | defaultSequenceNum = wire.MaxTxInSequenceNum - 10 70 | defaultRevealOutValue = int64(500) // 500 sat, ord default 10000 71 | 72 | MaxStandardTxWeight = blockchain.MaxBlockWeight / 10 73 | ) 74 | 75 | func NewInscriptionTool(net *chaincfg.Params, rpcclient *rpcclient.Client, request *InscriptionRequest) (*InscriptionTool, error) { 76 | tool := &InscriptionTool{ 77 | net: net, 78 | client: &blockchainClient{ 79 | rpcClient: rpcclient, 80 | }, 81 | commitTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), 82 | txCtxDataList: make([]*inscriptionTxCtxData, len(request.DataList)), 83 | revealTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), 84 | } 85 | return tool, tool._initTool(net, request) 86 | } 87 | 88 | func NewInscriptionToolWithBtcApiClient(net *chaincfg.Params, btcApiClient btcapi.BTCAPIClient, request *InscriptionRequest) (*InscriptionTool, error) { 89 | if len(request.CommitTxPrivateKeyList) != len(request.CommitTxOutPointList) { 90 | return nil, errors.New("the length of CommitTxPrivateKeyList and CommitTxOutPointList should be the same") 91 | } 92 | tool := &InscriptionTool{ 93 | net: net, 94 | client: &blockchainClient{ 95 | btcApiClient: btcApiClient, 96 | }, 97 | commitTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), 98 | commitTxPrivateKeyList: request.CommitTxPrivateKeyList, 99 | revealTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), 100 | } 101 | return tool, tool._initTool(net, request) 102 | } 103 | 104 | func (tool *InscriptionTool) _initTool(net *chaincfg.Params, request *InscriptionRequest) error { 105 | revealOutValue := defaultRevealOutValue 106 | if request.RevealOutValue > 0 { 107 | revealOutValue = request.RevealOutValue 108 | } 109 | tool.txCtxDataList = make([]*inscriptionTxCtxData, len(request.DataList)) 110 | destinations := make([]string, len(request.DataList)) 111 | for i := 0; i < len(request.DataList); i++ { 112 | txCtxData, err := createInscriptionTxCtxData(net, request.DataList[i]) 113 | if err != nil { 114 | return err 115 | } 116 | tool.txCtxDataList[i] = txCtxData 117 | destinations[i] = request.DataList[i].Destination 118 | } 119 | totalRevealPrevOutput, err := tool.buildEmptyRevealTx(request.SingleRevealTxOnly, destinations, revealOutValue, request.FeeRate) 120 | if err != nil { 121 | return err 122 | } 123 | err = tool.buildCommitTx(request.CommitTxOutPointList, totalRevealPrevOutput, request.CommitFeeRate) 124 | if err != nil { 125 | return err 126 | } 127 | err = tool.completeRevealTx() 128 | if err != nil { 129 | return err 130 | } 131 | err = tool.signCommitTx() 132 | if err != nil { 133 | return errors.Wrap(err, "sign commit tx error") 134 | } 135 | return err 136 | } 137 | 138 | func createInscriptionTxCtxData(net *chaincfg.Params, data InscriptionData) (*inscriptionTxCtxData, error) { 139 | privateKey, err := btcec.NewPrivateKey() 140 | if err != nil { 141 | return nil, err 142 | } 143 | inscriptionBuilder := txscript.NewScriptBuilder(). 144 | AddData(schnorr.SerializePubKey(privateKey.PubKey())). 145 | AddOp(txscript.OP_CHECKSIG). 146 | AddOp(txscript.OP_FALSE). 147 | AddOp(txscript.OP_IF). 148 | AddData([]byte("ord")). 149 | // Two OP_DATA_1 should be OP_1. However, in the following link, it's not set as OP_1: 150 | // https://github.com/casey/ord/blob/0.5.1/src/inscription.rs#L17 151 | // Therefore, we use two OP_DATA_1 to maintain consistency with ord. 152 | AddOp(txscript.OP_DATA_1). 153 | AddOp(txscript.OP_DATA_1). 154 | AddData([]byte(data.ContentType)). 155 | AddOp(txscript.OP_0) 156 | maxChunkSize := 520 157 | bodySize := len(data.Body) 158 | for i := 0; i < bodySize; i += maxChunkSize { 159 | end := i + maxChunkSize 160 | if end > bodySize { 161 | end = bodySize 162 | } 163 | // to skip txscript.MaxScriptSize 10000 164 | inscriptionBuilder.AddFullData(data.Body[i:end]) 165 | } 166 | inscriptionScript, err := inscriptionBuilder.Script() 167 | if err != nil { 168 | return nil, err 169 | } 170 | // to skip txscript.MaxScriptSize 10000 171 | inscriptionScript = append(inscriptionScript, txscript.OP_ENDIF) 172 | 173 | leafNode := txscript.NewBaseTapLeaf(inscriptionScript) 174 | proof := &txscript.TapscriptProof{ 175 | TapLeaf: leafNode, 176 | RootNode: leafNode, 177 | } 178 | 179 | controlBlock := proof.ToControlBlock(privateKey.PubKey()) 180 | controlBlockWitness, err := controlBlock.ToBytes() 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | tapHash := proof.RootNode.TapHash() 186 | commitTxAddress, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootOutputKey(privateKey.PubKey(), tapHash[:])), net) 187 | if err != nil { 188 | return nil, err 189 | } 190 | commitTxAddressPkScript, err := txscript.PayToAddrScript(commitTxAddress) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | recoveryPrivateKeyWIF, err := btcutil.NewWIF(txscript.TweakTaprootPrivKey(*privateKey, tapHash[:]), net, true) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return &inscriptionTxCtxData{ 201 | privateKey: privateKey, 202 | inscriptionScript: inscriptionScript, 203 | commitTxAddressPkScript: commitTxAddressPkScript, 204 | controlBlockWitness: controlBlockWitness, 205 | recoveryPrivateKeyWIF: recoveryPrivateKeyWIF.String(), 206 | }, nil 207 | } 208 | 209 | func (tool *InscriptionTool) buildEmptyRevealTx(singleRevealTxOnly bool, destination []string, revealOutValue, feeRate int64) (int64, error) { 210 | var revealTx []*wire.MsgTx 211 | totalPrevOutput := int64(0) 212 | total := len(tool.txCtxDataList) 213 | addTxInTxOutIntoRevealTx := func(tx *wire.MsgTx, index int) error { 214 | in := wire.NewTxIn(&wire.OutPoint{Index: uint32(index)}, nil, nil) 215 | in.Sequence = defaultSequenceNum 216 | tx.AddTxIn(in) 217 | receiver, err := btcutil.DecodeAddress(destination[index], tool.net) 218 | if err != nil { 219 | return err 220 | } 221 | scriptPubKey, err := txscript.PayToAddrScript(receiver) 222 | if err != nil { 223 | return err 224 | } 225 | out := wire.NewTxOut(revealOutValue, scriptPubKey) 226 | tx.AddTxOut(out) 227 | return nil 228 | } 229 | if singleRevealTxOnly { 230 | revealTx = make([]*wire.MsgTx, 1) 231 | tx := wire.NewMsgTx(wire.TxVersion) 232 | for i := 0; i < total; i++ { 233 | err := addTxInTxOutIntoRevealTx(tx, i) 234 | if err != nil { 235 | return 0, err 236 | } 237 | } 238 | eachRevealBaseTxFee := int64(tx.SerializeSize()) * feeRate / int64(total) 239 | prevOutput := (revealOutValue + eachRevealBaseTxFee) * int64(total) 240 | { 241 | emptySignature := make([]byte, 64) 242 | emptyControlBlockWitness := make([]byte, 33) 243 | for i := 0; i < total; i++ { 244 | fee := (int64(wire.TxWitness{emptySignature, tool.txCtxDataList[i].inscriptionScript, emptyControlBlockWitness}.SerializeSize()+2+3) / 4) * feeRate 245 | tool.txCtxDataList[i].revealTxPrevOutput = &wire.TxOut{ 246 | PkScript: tool.txCtxDataList[i].commitTxAddressPkScript, 247 | Value: revealOutValue + eachRevealBaseTxFee + fee, 248 | } 249 | prevOutput += fee 250 | } 251 | } 252 | totalPrevOutput = prevOutput 253 | revealTx[0] = tx 254 | } else { 255 | revealTx = make([]*wire.MsgTx, total) 256 | for i := 0; i < total; i++ { 257 | tx := wire.NewMsgTx(wire.TxVersion) 258 | err := addTxInTxOutIntoRevealTx(tx, i) 259 | if err != nil { 260 | return 0, err 261 | } 262 | prevOutput := revealOutValue + int64(tx.SerializeSize())*feeRate 263 | { 264 | emptySignature := make([]byte, 64) 265 | emptyControlBlockWitness := make([]byte, 33) 266 | fee := (int64(wire.TxWitness{emptySignature, tool.txCtxDataList[i].inscriptionScript, emptyControlBlockWitness}.SerializeSize()+2+3) / 4) * feeRate 267 | prevOutput += fee 268 | tool.txCtxDataList[i].revealTxPrevOutput = &wire.TxOut{ 269 | PkScript: tool.txCtxDataList[i].commitTxAddressPkScript, 270 | Value: prevOutput, 271 | } 272 | } 273 | totalPrevOutput += prevOutput 274 | revealTx[i] = tx 275 | } 276 | } 277 | tool.revealTx = revealTx 278 | return totalPrevOutput, nil 279 | } 280 | 281 | func (tool *InscriptionTool) getTxOutByOutPoint(outPoint *wire.OutPoint) (*wire.TxOut, error) { 282 | var txOut *wire.TxOut 283 | if tool.client.rpcClient != nil { 284 | tx, err := tool.client.rpcClient.GetRawTransactionVerbose(&outPoint.Hash) 285 | if err != nil { 286 | return nil, err 287 | } 288 | if int(outPoint.Index) >= len(tx.Vout) { 289 | return nil, errors.New("err out point") 290 | } 291 | vout := tx.Vout[outPoint.Index] 292 | pkScript, err := hex.DecodeString(vout.ScriptPubKey.Hex) 293 | if err != nil { 294 | return nil, err 295 | } 296 | amount, err := btcutil.NewAmount(vout.Value) 297 | if err != nil { 298 | return nil, err 299 | } 300 | txOut = wire.NewTxOut(int64(amount), pkScript) 301 | } else { 302 | tx, err := tool.client.btcApiClient.GetRawTransaction(&outPoint.Hash) 303 | if err != nil { 304 | return nil, err 305 | } 306 | if int(outPoint.Index) >= len(tx.TxOut) { 307 | return nil, errors.New("err out point") 308 | } 309 | txOut = tx.TxOut[outPoint.Index] 310 | } 311 | tool.commitTxPrevOutputFetcher.AddPrevOut(*outPoint, txOut) 312 | return txOut, nil 313 | } 314 | 315 | func (tool *InscriptionTool) buildCommitTx(commitTxOutPointList []*wire.OutPoint, totalRevealPrevOutput, commitFeeRate int64) error { 316 | totalSenderAmount := btcutil.Amount(0) 317 | tx := wire.NewMsgTx(wire.TxVersion) 318 | var changePkScript *[]byte 319 | for i := range commitTxOutPointList { 320 | txOut, err := tool.getTxOutByOutPoint(commitTxOutPointList[i]) 321 | if err != nil { 322 | return err 323 | } 324 | if changePkScript == nil { // first sender as change address 325 | changePkScript = &txOut.PkScript 326 | } 327 | in := wire.NewTxIn(commitTxOutPointList[i], nil, nil) 328 | in.Sequence = defaultSequenceNum 329 | tx.AddTxIn(in) 330 | 331 | totalSenderAmount += btcutil.Amount(txOut.Value) 332 | } 333 | for i := range tool.txCtxDataList { 334 | tx.AddTxOut(tool.txCtxDataList[i].revealTxPrevOutput) 335 | } 336 | 337 | tx.AddTxOut(wire.NewTxOut(0, *changePkScript)) 338 | fee := btcutil.Amount(mempool.GetTxVirtualSize(btcutil.NewTx(tx))) * btcutil.Amount(commitFeeRate) 339 | changeAmount := totalSenderAmount - btcutil.Amount(totalRevealPrevOutput) - fee 340 | if changeAmount > 0 { 341 | tx.TxOut[len(tx.TxOut)-1].Value = int64(changeAmount) 342 | } else { 343 | tx.TxOut = tx.TxOut[:len(tx.TxOut)-1] 344 | if changeAmount < 0 { 345 | feeWithoutChange := btcutil.Amount(mempool.GetTxVirtualSize(btcutil.NewTx(tx))) * btcutil.Amount(commitFeeRate) 346 | if totalSenderAmount-btcutil.Amount(totalRevealPrevOutput)-feeWithoutChange < 0 { 347 | return errors.New("insufficient balance") 348 | } 349 | } 350 | } 351 | tool.commitTx = tx 352 | return nil 353 | } 354 | 355 | func (tool *InscriptionTool) completeRevealTx() error { 356 | for i := range tool.txCtxDataList { 357 | tool.revealTxPrevOutputFetcher.AddPrevOut(wire.OutPoint{ 358 | Hash: tool.commitTx.TxHash(), 359 | Index: uint32(i), 360 | }, tool.txCtxDataList[i].revealTxPrevOutput) 361 | if len(tool.revealTx) == 1 { 362 | tool.revealTx[0].TxIn[i].PreviousOutPoint.Hash = tool.commitTx.TxHash() 363 | } else { 364 | tool.revealTx[i].TxIn[0].PreviousOutPoint.Hash = tool.commitTx.TxHash() 365 | } 366 | } 367 | witnessList := make([]wire.TxWitness, len(tool.txCtxDataList)) 368 | for i := range tool.txCtxDataList { 369 | revealTx := tool.revealTx[0] 370 | idx := i 371 | if len(tool.revealTx) != 1 { 372 | revealTx = tool.revealTx[i] 373 | idx = 0 374 | } 375 | witnessArray, err := txscript.CalcTapscriptSignaturehash(txscript.NewTxSigHashes(revealTx, tool.revealTxPrevOutputFetcher), 376 | txscript.SigHashDefault, revealTx, idx, tool.revealTxPrevOutputFetcher, txscript.NewBaseTapLeaf(tool.txCtxDataList[i].inscriptionScript)) 377 | if err != nil { 378 | return err 379 | } 380 | signature, err := schnorr.Sign(tool.txCtxDataList[i].privateKey, witnessArray) 381 | if err != nil { 382 | return err 383 | } 384 | witnessList[i] = wire.TxWitness{signature.Serialize(), tool.txCtxDataList[i].inscriptionScript, tool.txCtxDataList[i].controlBlockWitness} 385 | } 386 | for i := range witnessList { 387 | if len(tool.revealTx) == 1 { 388 | tool.revealTx[0].TxIn[i].Witness = witnessList[i] 389 | } else { 390 | tool.revealTx[i].TxIn[0].Witness = witnessList[i] 391 | } 392 | } 393 | // check tx max tx wight 394 | for i, tx := range tool.revealTx { 395 | revealWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) 396 | if revealWeight > MaxStandardTxWeight { 397 | return errors.New(fmt.Sprintf("reveal(index %d) transaction weight greater than %d (MAX_STANDARD_TX_WEIGHT): %d", i, MaxStandardTxWeight, revealWeight)) 398 | } 399 | } 400 | return nil 401 | } 402 | 403 | func (tool *InscriptionTool) signCommitTx() error { 404 | if len(tool.commitTxPrivateKeyList) == 0 { 405 | commitSignTransaction, isSignComplete, err := tool.client.rpcClient.SignRawTransactionWithWallet(tool.commitTx) 406 | if err != nil { 407 | log.Printf("sign commit tx error, %v", err) 408 | return err 409 | } 410 | if !isSignComplete { 411 | return errors.New("sign commit tx error") 412 | } 413 | tool.commitTx = commitSignTransaction 414 | } else { 415 | witnessList := make([]wire.TxWitness, len(tool.commitTx.TxIn)) 416 | for i := range tool.commitTx.TxIn { 417 | txOut := tool.commitTxPrevOutputFetcher.FetchPrevOutput(tool.commitTx.TxIn[i].PreviousOutPoint) 418 | witness, err := txscript.TaprootWitnessSignature(tool.commitTx, txscript.NewTxSigHashes(tool.commitTx, tool.commitTxPrevOutputFetcher), 419 | i, txOut.Value, txOut.PkScript, txscript.SigHashDefault, tool.commitTxPrivateKeyList[i]) 420 | if err != nil { 421 | return err 422 | } 423 | witnessList[i] = witness 424 | } 425 | for i := range witnessList { 426 | tool.commitTx.TxIn[i].Witness = witnessList[i] 427 | } 428 | } 429 | return nil 430 | } 431 | 432 | func (tool *InscriptionTool) BackupRecoveryKeyToRpcNode() error { 433 | if tool.client.rpcClient == nil { 434 | return errors.New("rpc client is nil") 435 | } 436 | descriptors := make([]extRpcClient.Descriptor, len(tool.txCtxDataList)) 437 | for i := range tool.txCtxDataList { 438 | descriptorInfo, err := tool.client.rpcClient.GetDescriptorInfo(fmt.Sprintf("rawtr(%s)", tool.txCtxDataList[i].recoveryPrivateKeyWIF)) 439 | if err != nil { 440 | return err 441 | } 442 | descriptors[i] = extRpcClient.Descriptor{ 443 | Desc: *btcjson.String(fmt.Sprintf("rawtr(%s)#%s", tool.txCtxDataList[i].recoveryPrivateKeyWIF, descriptorInfo.Checksum)), 444 | Timestamp: btcjson.TimestampOrNow{ 445 | Value: "now", 446 | }, 447 | Active: btcjson.Bool(false), 448 | Range: nil, 449 | NextIndex: nil, 450 | Internal: btcjson.Bool(false), 451 | Label: btcjson.String("commit tx recovery key"), 452 | } 453 | } 454 | results, err := extRpcClient.ImportDescriptors(tool.client.rpcClient, descriptors) 455 | if err != nil { 456 | return err 457 | } 458 | if results == nil { 459 | return errors.New("commit tx recovery key import failed, nil result") 460 | } 461 | for _, result := range *results { 462 | if !result.Success { 463 | return errors.New("commit tx recovery key import failed") 464 | } 465 | } 466 | return nil 467 | } 468 | 469 | func (tool *InscriptionTool) GetRecoveryKeyWIFList() []string { 470 | wifList := make([]string, len(tool.txCtxDataList)) 471 | for i := range tool.txCtxDataList { 472 | wifList[i] = tool.txCtxDataList[i].recoveryPrivateKeyWIF 473 | } 474 | return wifList 475 | } 476 | 477 | func getTxHex(tx *wire.MsgTx) (string, error) { 478 | var buf bytes.Buffer 479 | if err := tx.Serialize(&buf); err != nil { 480 | return "", err 481 | } 482 | return hex.EncodeToString(buf.Bytes()), nil 483 | } 484 | 485 | func (tool *InscriptionTool) GetCommitTxHex() (string, error) { 486 | return getTxHex(tool.commitTx) 487 | } 488 | 489 | func (tool *InscriptionTool) GetRevealTxHexList() ([]string, error) { 490 | txHexList := make([]string, len(tool.revealTx)) 491 | for i := range tool.revealTx { 492 | txHex, err := getTxHex(tool.revealTx[i]) 493 | if err != nil { 494 | return nil, err 495 | } 496 | txHexList[i] = txHex 497 | } 498 | return txHexList, nil 499 | } 500 | 501 | func (tool *InscriptionTool) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { 502 | if tool.client.rpcClient != nil { 503 | return tool.client.rpcClient.SendRawTransaction(tx, false) 504 | } else { 505 | return tool.client.btcApiClient.BroadcastTx(tx) 506 | } 507 | } 508 | 509 | func (tool *InscriptionTool) calculateFee() int64 { 510 | fees := int64(0) 511 | for _, in := range tool.commitTx.TxIn { 512 | fees += tool.commitTxPrevOutputFetcher.FetchPrevOutput(in.PreviousOutPoint).Value 513 | } 514 | for _, out := range tool.commitTx.TxOut { 515 | fees -= out.Value 516 | } 517 | for _, tx := range tool.revealTx { 518 | for _, in := range tx.TxIn { 519 | fees += tool.revealTxPrevOutputFetcher.FetchPrevOutput(in.PreviousOutPoint).Value 520 | } 521 | for _, out := range tx.TxOut { 522 | fees -= out.Value 523 | } 524 | } 525 | return fees 526 | } 527 | 528 | func (tool *InscriptionTool) Inscribe() (commitTxHash *chainhash.Hash, revealTxHashList []*chainhash.Hash, inscriptions []string, fees int64, err error) { 529 | fees = tool.calculateFee() 530 | commitTxHash, err = tool.sendRawTransaction(tool.commitTx) 531 | if err != nil { 532 | return nil, nil, nil, fees, errors.Wrap(err, "send commit tx error") 533 | } 534 | revealTxHashList = make([]*chainhash.Hash, len(tool.revealTx)) 535 | inscriptions = make([]string, len(tool.txCtxDataList)) 536 | for i := range tool.revealTx { 537 | _revealTxHash, err := tool.sendRawTransaction(tool.revealTx[i]) 538 | if err != nil { 539 | return commitTxHash, revealTxHashList, nil, fees, errors.Wrap(err, fmt.Sprintf("send reveal tx error, %d。", i)) 540 | } 541 | revealTxHashList[i] = _revealTxHash 542 | if len(tool.revealTx) == len(tool.txCtxDataList) { 543 | inscriptions[i] = fmt.Sprintf("%si0", _revealTxHash) 544 | } else { 545 | inscriptions[i] = fmt.Sprintf("%si", _revealTxHash) 546 | } 547 | } 548 | if len(tool.revealTx) != len(tool.txCtxDataList) { 549 | for i := len(inscriptions) - 1; i > 0; i-- { 550 | inscriptions[i] = fmt.Sprintf("%s%d", inscriptions[0], i) 551 | } 552 | } 553 | return commitTxHash, revealTxHashList, inscriptions, fees, nil 554 | } 555 | -------------------------------------------------------------------------------- /pkg/btcapi/btcapi.go: -------------------------------------------------------------------------------- 1 | package btcapi 2 | 3 | import ( 4 | "fmt" 5 | "github.com/btcsuite/btcd/btcutil" 6 | "github.com/btcsuite/btcd/chaincfg/chainhash" 7 | "github.com/btcsuite/btcd/wire" 8 | "github.com/pkg/errors" 9 | "io" 10 | "net/http" 11 | ) 12 | 13 | type UnspentOutput struct { 14 | Outpoint *wire.OutPoint 15 | Output *wire.TxOut 16 | } 17 | 18 | type BTCAPIClient interface { 19 | GetRawTransaction(txHash *chainhash.Hash) (*wire.MsgTx, error) 20 | BroadcastTx(tx *wire.MsgTx) (*chainhash.Hash, error) 21 | ListUnspent(address btcutil.Address) ([]*UnspentOutput, error) 22 | } 23 | 24 | func Request(method, baseURL, subPath string, requestBody io.Reader) ([]byte, error) { 25 | url := fmt.Sprintf("%s%s", baseURL, subPath) 26 | req, err := http.NewRequest(method, url, requestBody) 27 | if err != nil { 28 | return nil, errors.Wrap(err, "failed to create request") 29 | } 30 | req.Header.Add("Content-Type", "application/json") 31 | req.Header.Add("Accept", "application/json") 32 | resp, err := http.DefaultClient.Do(req) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "failed to send request") 35 | } 36 | defer resp.Body.Close() 37 | body, err := io.ReadAll(resp.Body) 38 | if err != nil { 39 | return nil, errors.Wrap(err, "failed to read response body") 40 | } 41 | return body, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/btcapi/mempool/addresses.go: -------------------------------------------------------------------------------- 1 | package mempool 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/btcsuite/btcd/btcutil" 7 | "github.com/btcsuite/btcd/chaincfg/chainhash" 8 | "github.com/btcsuite/btcd/wire" 9 | "github.com/vincentdebug/go-ord-tx/pkg/btcapi" 10 | "net/http" 11 | ) 12 | 13 | type UTXO struct { 14 | Txid string `json:"txid"` 15 | Vout int `json:"vout"` 16 | Status struct { 17 | Confirmed bool `json:"confirmed"` 18 | BlockHeight int `json:"block_height"` 19 | BlockHash string `json:"block_hash"` 20 | BlockTime int64 `json:"block_time"` 21 | } `json:"status"` 22 | Value int64 `json:"value"` 23 | } 24 | 25 | // UTXOs is a slice of UTXO 26 | type UTXOs []UTXO 27 | 28 | func (c *MempoolClient) ListUnspent(address btcutil.Address) ([]*btcapi.UnspentOutput, error) { 29 | res, err := c.request(http.MethodGet, fmt.Sprintf("/address/%s/utxo", address.EncodeAddress()), nil) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | var utxos UTXOs 35 | err = json.Unmarshal(res, &utxos) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | unspentOutputs := make([]*btcapi.UnspentOutput, 0) 41 | for _, utxo := range utxos { 42 | txHash, err := chainhash.NewHashFromStr(utxo.Txid) 43 | if err != nil { 44 | return nil, err 45 | } 46 | unspentOutputs = append(unspentOutputs, &btcapi.UnspentOutput{ 47 | Outpoint: wire.NewOutPoint(txHash, uint32(utxo.Vout)), 48 | Output: wire.NewTxOut(utxo.Value, address.ScriptAddress()), 49 | }) 50 | } 51 | return unspentOutputs, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/btcapi/mempool/addresses_test.go: -------------------------------------------------------------------------------- 1 | package mempool 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/btcutil" 5 | "github.com/btcsuite/btcd/chaincfg" 6 | "testing" 7 | ) 8 | 9 | func TestListUnspent(t *testing.T) { 10 | // https://mempool.space/signet/api/address/tb1p8lh4np5824u48ppawq3numsm7rss0de4kkxry0z70dcfwwwn2fcspyyhc7/utxo 11 | netParams := &chaincfg.SigNetParams 12 | client := NewClient(netParams) 13 | address, _ := btcutil.DecodeAddress("tb1p8lh4np5824u48ppawq3numsm7rss0de4kkxry0z70dcfwwwn2fcspyyhc7", netParams) 14 | unspentList, err := client.ListUnspent(address) 15 | if err != nil { 16 | t.Error(err) 17 | } else { 18 | t.Log(len(unspentList)) 19 | for _, output := range unspentList { 20 | t.Log(output.Outpoint.Hash.String(), " ", output.Outpoint.Index) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/btcapi/mempool/client.go: -------------------------------------------------------------------------------- 1 | package mempool 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/chaincfg" 5 | "github.com/btcsuite/btcd/wire" 6 | "github.com/vincentdebug/go-ord-tx/pkg/btcapi" 7 | "io" 8 | "log" 9 | ) 10 | 11 | type MempoolClient struct { 12 | baseURL string 13 | } 14 | 15 | func NewClient(netParams *chaincfg.Params) *MempoolClient { 16 | baseURL := "" 17 | if netParams.Net == wire.MainNet { 18 | baseURL = "https://mempool.space/api" 19 | } else if netParams.Net == wire.TestNet3 { 20 | baseURL = "https://mempool.space/testnet/api" 21 | } else if netParams.Net == chaincfg.SigNetParams.Net { 22 | baseURL = "https://mempool.space/signet/api" 23 | } else { 24 | log.Fatal("mempool don't support other netParams") 25 | } 26 | return &MempoolClient{ 27 | baseURL: baseURL, 28 | } 29 | } 30 | 31 | func (c *MempoolClient) request(method, subPath string, requestBody io.Reader) ([]byte, error) { 32 | return btcapi.Request(method, c.baseURL, subPath, requestBody) 33 | } 34 | 35 | var _ btcapi.BTCAPIClient = (*MempoolClient)(nil) 36 | -------------------------------------------------------------------------------- /pkg/btcapi/mempool/transactions.go: -------------------------------------------------------------------------------- 1 | package mempool 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "github.com/btcsuite/btcd/chaincfg/chainhash" 8 | "github.com/btcsuite/btcd/wire" 9 | "github.com/pkg/errors" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | func (c *MempoolClient) GetRawTransaction(txHash *chainhash.Hash) (*wire.MsgTx, error) { 15 | res, err := c.request(http.MethodGet, fmt.Sprintf("/tx/%s/raw", txHash.String()), nil) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | tx := wire.NewMsgTx(wire.TxVersion) 21 | if err := tx.Deserialize(bytes.NewReader(res)); err != nil { 22 | return nil, err 23 | } 24 | return tx, nil 25 | } 26 | 27 | func (c *MempoolClient) BroadcastTx(tx *wire.MsgTx) (*chainhash.Hash, error) { 28 | var buf bytes.Buffer 29 | if err := tx.Serialize(&buf); err != nil { 30 | return nil, err 31 | } 32 | 33 | res, err := c.request(http.MethodPost, "/tx", strings.NewReader(hex.EncodeToString(buf.Bytes()))) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | txHash, err := chainhash.NewHashFromStr(string(res)) 39 | if err != nil { 40 | return nil, errors.Wrap(err, fmt.Sprintf("failed to parse tx hash, %s", string(res))) 41 | } 42 | return txHash, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/btcapi/mempool/transactions_test.go: -------------------------------------------------------------------------------- 1 | package mempool 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/chaincfg" 5 | "github.com/btcsuite/btcd/chaincfg/chainhash" 6 | "testing" 7 | ) 8 | 9 | func TestGetRawTransaction(t *testing.T) { 10 | //https://mempool.space/signet/api/tx/b752d80e97196582fd02303f76b4b886c222070323fb7ccd425f6c89f5445f6c/hex 11 | client := NewClient(&chaincfg.SigNetParams) 12 | txId, _ := chainhash.NewHashFromStr("b752d80e97196582fd02303f76b4b886c222070323fb7ccd425f6c89f5445f6c") 13 | transaction, err := client.GetRawTransaction(txId) 14 | if err != nil { 15 | t.Error(err) 16 | } else { 17 | t.Log(transaction.TxHash().String()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/rpcclient/import_descriptors_cmds.go: -------------------------------------------------------------------------------- 1 | package rpcclient 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/btcsuite/btcd/btcjson" 6 | "github.com/btcsuite/btcd/rpcclient" 7 | ) 8 | 9 | type Descriptor struct { 10 | Desc string `json:"desc"` 11 | Active *bool `json:"active,omitempty"` 12 | Range interface{} `json:"range,omitempty"` 13 | NextIndex *int `json:"next_index,omitempty"` 14 | Timestamp interface{} `json:"timestamp"` 15 | Internal *bool `json:"internal,omitempty"` 16 | Label *string `json:"label,omitempty"` 17 | } 18 | 19 | // ImportDescriptorsCmd @see https://developer.bitcoin.org/reference/rpc/importdescriptors.html 20 | type ImportDescriptorsCmd struct { 21 | Descriptors []Descriptor `json:""` 22 | } 23 | 24 | func NewImportDescriptorsCmd(descriptors []Descriptor) *ImportDescriptorsCmd { 25 | return &ImportDescriptorsCmd{ 26 | Descriptors: descriptors, 27 | } 28 | } 29 | 30 | type ImportDescriptorsResultElement struct { 31 | Success bool `json:"success"` 32 | Warnings []string `json:"warnings,omitempty"` 33 | Error *btcjson.RPCError `json:"error,omitempty"` 34 | } 35 | 36 | type ImportDescriptorsResult []ImportDescriptorsResultElement 37 | 38 | type FutureImportDescriptorsResult chan *rpcclient.Response 39 | 40 | func (r FutureImportDescriptorsResult) Receive() (*ImportDescriptorsResult, error) { 41 | res, err := rpcclient.ReceiveFuture(r) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | var importDescriptors ImportDescriptorsResult 47 | err = json.Unmarshal(res, &importDescriptors) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &importDescriptors, nil 53 | } 54 | 55 | func ImportDescriptorsAsync(c *rpcclient.Client, descriptors []Descriptor) FutureImportDescriptorsResult { 56 | cmd := &ImportDescriptorsCmd{ 57 | Descriptors: descriptors, 58 | } 59 | return c.SendCmd(cmd) 60 | } 61 | 62 | func ImportDescriptors(c *rpcclient.Client, descriptors []Descriptor) (*ImportDescriptorsResult, error) { 63 | return ImportDescriptorsAsync(c, descriptors).Receive() 64 | } 65 | 66 | func init() { 67 | flags := btcjson.UsageFlag(0) 68 | btcjson.MustRegisterCmd("importdescriptors", (*ImportDescriptorsCmd)(nil), flags) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/rpcclient/import_descriptors_cmds_test.go: -------------------------------------------------------------------------------- 1 | package rpcclient 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/btcsuite/btcd/btcec/v2" 7 | "github.com/btcsuite/btcd/btcjson" 8 | "github.com/btcsuite/btcd/btcutil" 9 | "github.com/btcsuite/btcd/chaincfg" 10 | "github.com/btcsuite/btcd/rpcclient" 11 | "log" 12 | "testing" 13 | ) 14 | 15 | func TestImportDescriptorsCmds(t *testing.T) { 16 | connCfg := &rpcclient.ConnConfig{ 17 | Host: "localhost:8336", 18 | User: "yourrpcuser", 19 | Pass: "yourrpcpass", 20 | HTTPPostMode: true, 21 | DisableTLS: true, 22 | } 23 | client, err := rpcclient.New(connCfg, nil) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | defer client.Shutdown() 28 | 29 | net := &chaincfg.SigNetParams 30 | 31 | privateKey, err := btcec.NewPrivateKey() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | privateKeyWIF, err := btcutil.NewWIF(privateKey, net, true) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | descriptorInfo, err := client.GetDescriptorInfo(fmt.Sprintf("rawtr(%s)", privateKeyWIF)) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | descriptors := []Descriptor{ 46 | { 47 | 48 | Desc: *btcjson.String(fmt.Sprintf("rawtr(%s)#%s", privateKeyWIF, descriptorInfo.Checksum)), 49 | Timestamp: btcjson.TimestampOrNow{ 50 | Value: "now", 51 | }, 52 | Active: btcjson.Bool(false), 53 | Range: nil, 54 | NextIndex: nil, 55 | Internal: btcjson.Bool(false), 56 | Label: btcjson.String("test label"), 57 | }, 58 | } 59 | 60 | results, err := ImportDescriptors(client, descriptors) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | if results == nil { 65 | log.Fatalf("import failed, nil result") 66 | } 67 | for _, result := range *results { 68 | if !result.Success { 69 | log.Fatal(errors.New("import failed")) 70 | } 71 | } 72 | log.Printf("Import descriptors success.") 73 | } 74 | --------------------------------------------------------------------------------