├── .gitignore ├── LICENSE ├── README.md ├── atomicals-api ├── atomicals.api ├── etc │ ├── config.json │ └── main-api.yaml ├── internal │ ├── config │ │ └── config.go │ ├── handler │ │ ├── checktxhandler.go │ │ ├── getassetbylocationidhandler.go │ │ ├── getassetbyuserpkhandler.go │ │ └── routes.go │ ├── logic │ │ ├── checktxlogic.go │ │ ├── getassetbylocationidlogic.go │ │ └── getassetbyuserpklogic.go │ ├── svc │ │ ├── servicecontext.go │ │ └── syncAtomicalsAsset.go │ └── types │ │ └── types.go └── main.go ├── atomicals-indexer ├── atomicals-core │ ├── operation │ │ ├── atomicals.go │ │ ├── merkleVerify.go │ │ ├── operationDat.go │ │ ├── operationDft.go │ │ ├── operationDmt.go │ │ ├── operationFt.go │ │ ├── operationMod.go │ │ ├── operationNft.go │ │ ├── referenceIndexer.go │ │ ├── rule.go │ │ ├── syncTxHeight.go │ │ ├── traceTx.go │ │ ├── transferFt.go │ │ └── transferNft.go │ └── witness │ │ ├── check.go │ │ ├── operation.go │ │ ├── payload.go │ │ ├── python-parse │ │ ├── api.go │ │ ├── enumeration.py │ │ ├── parse.py │ │ └── util.py │ │ └── witness.go └── main.go ├── conf ├── config.json └── configfractalbitcoin.json ├── doc ├── 0.atomicalsCoreFramework.md ├── 1.utxoColor.md ├── 2.atomicalsProtocal.md ├── 3.dft.md ├── 4.dmt.md ├── 5.nft.md ├── 6.ft.md ├── pic │ └── atomicals-go-framework.png └── proposal │ ├── split.md │ └── swap.md ├── go.mod ├── go.sum ├── pkg ├── btcsync │ ├── account.go │ ├── block.go │ ├── sync.go │ └── tx.go ├── conf │ └── config.go ├── errors │ ├── api.go │ ├── const.go │ └── error.go ├── log │ └── zaplog.go └── merkle │ └── merkle.go ├── repo ├── api.go ├── atomicalsTx.go ├── bloomFilter.go ├── db.go ├── ftRead.go ├── mod.go ├── nftRead.go └── postsql │ ├── atomicalsDat.go │ ├── atomicalsGlobalDirectFt.go │ ├── atomicalsGlobalDistributedFt.go │ ├── atomicalsLocation.go │ ├── atomicalsMod.go │ ├── atomicalsPayment.go │ ├── atomicalsTx.go │ ├── atomicalsUTXOFt.go │ ├── atomicalsUTXONft.go │ ├── bloomFilter.go │ ├── init │ └── init.go │ ├── model.go │ └── statistic.go ├── testdata └── mainnet.md └── utils ├── atomicalsID.go ├── bitwork.go ├── blockHeight.go ├── byte.go ├── constant.go ├── name.go ├── scriptPubKey.go ├── sha256 ├── constant.go ├── math.go ├── sha.go └── sum.go ├── string.go ├── unused.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | # config file 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 yiming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atomicals-go ⚛️ 2 | 3 | #### atomicals-go 是什么 4 | - Atomicals: 是一个使用染色方法在BTC链上发行资产的协议,作者[Arthur's twitter(X)](https://twitter.com/atomicalsxyz),该协议包括:realm,nft和ft 5 | - 目前Arthur 并未以文档或protocal形式披露atomicals的具体内容。但是提供了一个python版本的实现,包括: 6 | - atomicals索引器[atomicals-electrumx](https://github.com/atomicals/atomicals-electrumx) 7 | - atomicals交易发送工具[atomicals-js](https://github.com/atomicals/atomicals-js)命令工具 8 | - Atomicals-go: 是atomicals索引器atomicals-electrumx的golang版本,并以文本方式提供了atomicals协议的详细内容(在本仓库的doc目录下) 9 | 10 | - 在未来一段时间内[github:yimingWOW](https://github.com/yimingWOW)仍然会维护该项目(及时同步atomicals-electrumx的更新) 11 | - 如果您想加入,可以通过twitter联系我:[x:@isyiming](https://twitter.com/isyiming) 12 | - 或者为我捐款: bc1p7uaqs0qq40mxqyljd93raxullh0ece2xvns5s5y9700v4ec0qjmsdt2q2n 接受任何类型的资产 13 | 14 | #### 嗨,atomicals-go终于完成了,我简单说一下这个indexer的优点 15 | 16 | - 占用更少的存储空间:一个完整的btc全节点需要730GB的磁盘空间。atomicals-go不需要btc全节点,你只需要以prune mode运行btc node,在你的电脑中只保存从808080高度开始的区块即可(这些区块大概占用大概140GB)btc链上和atomicals协议无关的信息全部被过滤,atomicals-go只将有效数据都存储在sql中,这部份数据不超过1GB 17 | - 防宕机:可以随时终止运行服务,即使是因为断电或者电脑死机等原因导致服务中断,没关系,只需要重启服务。它会在之前的区块高度继续同步,并且保证继续写入的数据是正确的 18 | - 适应btc链分叉:无需担心btc链分叉的影响,保证通过atomicals-go查到的atomicals永远是最新的正确的,并且包括最新区块 19 | - 支持查询mempool中的交易:即使某笔交易还没有被打包,只要你运行的btc节点可以查询到mempool中的交易,你就可以通过接口查看这笔atomicals交易包含的资产详情 20 | 21 | #### Performance 22 | - atomicals-core will spend 2.5s per block. if currentBlockHeight=834773, it will take about 20 hours to sync all btc blocks 23 | - 同步耗时平均2~4s/block,一天左右可以同步完成 24 | 25 | #### code counter 26 | 27 | | language | files | code | comment | blank | total | 28 | | :--- | ---: | ---: | ---: | ---: | ---: | 29 | | go | 70 | 4,268 | 198 | 520 | 4,986 | 30 | 31 | - 整个项目只有4k多行,我比较满意了,但是还存在很多冗余代码,有优化的空间 32 | - 函数,变量和文件命名还存在不规范的地方,有可能我一开始随便写了个名字,后面习惯了也就意识不到哪里命名不够见名知意了 33 | - 存在不够直接易懂的函数逻辑,和以上同因,如果你觉得哪里读起来太绕,麻烦提issue或者pr帮我纠正 34 | 35 | #### framework 36 | ![image](https://github.com/atomicals-community/atomicals-go/blob/main/doc/pic/atomicals-go-framework.png) 37 | 38 | 39 | 40 | #### TODO: 41 | - 我发现payment没有什么用,所以atomicals-go没有保存任何payment信息,如果有必要,希望有人来完成它 42 | - 为api-service服务提供更多必要的http接口, 由于我个人用不到任何http接口,所以不清楚那些接口是必要的,只提供了几个作为示例:getassetbyuserpk getassetbylocationid 43 | - checktx接口很重要,它是保证atomicals-go避免btc分叉影响的核心,它能够同步安全区块间隔以上的交易和mempool中的交易,但是其返回值格式化不彻底。希望有人来规范它;同样的原因,我不清楚atomicals的其他项目需要什么样的参数,大家可以定制不同的返回结构体,提交pr 44 | - http接口中应该加入必要的缓存 45 | 46 | - hey, 关心atomicals的各位小伙伴们,之前和wizz的成员沟通后,todo list中的payment也很重要,我将把这个功能补齐,这还需要一段时间。 47 | - 后续payment功能完成后,我将联系一些社区看看有没有人愿意运行这个索引器,我再提供一个简单的前端,提供某个token的历史持仓记录功能。引导更多人使用它,方便检测出索引器是否还存在位置bug。等到确认功能完备且索引正确后,我将在此基础上实现avm,任何人对此感兴趣的话,欢迎在twitter或者github issue中联系我 48 | 49 | ## How to run atomicals-go 50 | 1. run a local btc node 51 | ``` 52 | // cd to a path u want to save btc node file 53 | mkdir btc 54 | 55 | wget https://bitcoincore.org/bin/bitcoin-core-26.0/bitcoin-26.0-arm64-apple-darwin.tar.gz 56 | 57 | tar -xzvf bitcoin-26.0-x86_64-linux-gnu.tar.gz 58 | 59 | mv bitcoin-26.0 bitcoin 60 | 61 | vim ./bitcoin/bitcoin.conf 62 | 63 | ``` 64 | ``` 65 | Edit bitcoin.conf, add these params for main net. we run btc node with prune mode and set assumevalid=0000000000000000000211eb82135b8f5d8be921debf8eff1d6b38b73bc03834. 66 | Atomicals protocal start from blockHeight=808080, we don't need all blockInfo. 67 | 68 | # Options for mainnet 69 | [main] 70 | dbcache=1024 71 | server=1 72 | rest=1 73 | daemon=1 74 | rpcbind=0.0.0.0:8332 75 | rpcallowip=0.0.0.0/0 76 | rpcuser=btc 77 | rpcpassword=btc2012 78 | prune=240000 79 | assumevalid=0000000000000000000211eb82135b8f5d8be921debf8eff1d6b38b73bc03834 80 | ``` 81 | 82 | 2. install golang and docker 83 | 3. start a postgres sql by docker 84 | ``` 85 | $ docker run --name postgres -p 5432:5432 -e POSTGRES_DB=atomicals -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin123 -d postgres:14 86 | ``` 87 | 4. run atomicals indexer 88 | download atomicals-core 89 | - edit conf/config.json update it with your btc node url, user and password, and sql_dns: 90 | ``` 91 | { 92 | "btc_rpc_url" : "0.0.0.0:8332", 93 | "btc_rpc_user": "btc" , 94 | "btc_rpc_password": "btc2012", 95 | "sql_dns": "host=127.0.0.1 user=admin password=admin123 dbname=atomicals port=5432 sslmode=disable" 96 | } 97 | ``` 98 | ``` 99 | // cd to atomicals-go path 100 | go mod tidy 101 | 102 | // init sql table 103 | cd repo/postsql/init/ 104 | go run ./ 105 | 106 | // start indexer 107 | cd atomicals-indexer/ 108 | go run ./ 109 | // or run it with nohup: nohup go run ./ > log.txt 2>&1 & 110 | ``` 111 | // start atomicals-api service if you need 112 | ``` 113 | cd atomicals-api 114 | go run ./ 115 | ``` 116 | -------------------------------------------------------------------------------- /atomicals-api/atomicals.api: -------------------------------------------------------------------------------- 1 | type ( 2 | ReqAssetByLocationID { 3 | LocationID string `json:"location_id"` 4 | } 5 | RespAssetByLocationID { 6 | Assets []*UTXONftInfo `json:"assets"` 7 | } 8 | ReqAssetByUserPK { 9 | UserPK string `json:"user_pk"` 10 | } 11 | RespAssetByUserPK { 12 | Assets []*UTXONftInfo `json:"assets"` 13 | } 14 | ReqCheckTx { 15 | Txid string `json:"tx_id"` 16 | } 17 | RespCheckTx { 18 | Description string `json:"description"` 19 | } 20 | ) 21 | 22 | service main-api { 23 | @handler getAssetByLocationIDHandler 24 | get /api/v1/getAssetByLocationID (ReqAssetByLocationID) returns (RespAssetByLocationID) 25 | 26 | @handler getAssetByUserPkHandler 27 | get /api/v1/getAssetByUserPk (ReqAssetByUserPK) returns (RespAssetByUserPK) 28 | 29 | @handler checkTx 30 | get /api/v1/checkTx (ReqCheckTx) returns (RespCheckTx) 31 | } 32 | 33 | type UTXONftInfo { 34 | UserPk string 35 | AtomicalsID string `json:"atomicals_id"` 36 | LocationID string `json:"location_id"` 37 | RealmName string `json:"realm_name"` 38 | SubRealmName string `json:"subrealm_name"` 39 | ParentRealmAtomicalsID string `json:"parent_realm_atomicals_id"` 40 | ContainerName string `json:"container_name"` 41 | Dmitem string `json:"dmitem"` 42 | ParentContainerAtomicalsID string `json:"parent_container_atomicals_id"` 43 | Nonce int64 `json:"nonce"` 44 | Time int64 `json:"time"` 45 | Bitworkc string `json:"bitworkc"` 46 | Bitworkr string `json:"bitworkr"` 47 | } 48 | 49 | -------------------------------------------------------------------------------- /atomicals-api/etc/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "btc_rpc_url" : "0.0.0.0:8332", 3 | "btc_rpc_user": "btc" , 4 | "btc_rpc_password": "btc2012", 5 | "sql_dns": "host=127.0.0.1 user=admin password=admin123 dbname=atomicals port=5432 sslmode=disable" 6 | } -------------------------------------------------------------------------------- /atomicals-api/etc/main-api.yaml: -------------------------------------------------------------------------------- 1 | Name: main-api 2 | Host: 0.0.0.0 3 | Port: 8888 4 | SqlDNS: "host=127.0.0.1 user=admin password=admin123 dbname=atomicals port=5432 sslmode=disable" 5 | -------------------------------------------------------------------------------- /atomicals-api/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/zeromicro/go-zero/rest" 4 | 5 | type Config struct { 6 | rest.RestConf 7 | } 8 | -------------------------------------------------------------------------------- /atomicals-api/internal/handler/checktxhandler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/atomicals-go/atomicals-api/internal/logic" 7 | "github.com/atomicals-go/atomicals-api/internal/svc" 8 | "github.com/atomicals-go/atomicals-api/internal/types" 9 | "github.com/zeromicro/go-zero/rest/httpx" 10 | ) 11 | 12 | func checkTxHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | var req types.ReqCheckTx 15 | if err := httpx.Parse(r, &req); err != nil { 16 | httpx.ErrorCtx(r.Context(), w, err) 17 | return 18 | } 19 | 20 | l := logic.NewCheckTxLogic(r.Context(), svcCtx) 21 | resp, err := l.CheckTx(&req) 22 | if err != nil { 23 | httpx.ErrorCtx(r.Context(), w, err) 24 | } else { 25 | httpx.OkJsonCtx(r.Context(), w, resp) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /atomicals-api/internal/handler/getassetbylocationidhandler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/atomicals-go/atomicals-api/internal/logic" 7 | "github.com/atomicals-go/atomicals-api/internal/svc" 8 | "github.com/atomicals-go/atomicals-api/internal/types" 9 | "github.com/zeromicro/go-zero/rest/httpx" 10 | ) 11 | 12 | func getAssetByLocationIDHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | var req types.ReqAssetByLocationID 15 | if err := httpx.Parse(r, &req); err != nil { 16 | httpx.ErrorCtx(r.Context(), w, err) 17 | return 18 | } 19 | 20 | l := logic.NewGetassetByLocationIDLogic(r.Context(), svcCtx) 21 | resp, err := l.GetAssetByLocationID(&req) 22 | if err != nil { 23 | httpx.ErrorCtx(r.Context(), w, err) 24 | } else { 25 | httpx.OkJsonCtx(r.Context(), w, resp) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /atomicals-api/internal/handler/getassetbyuserpkhandler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/atomicals-go/atomicals-api/internal/logic" 7 | "github.com/atomicals-go/atomicals-api/internal/svc" 8 | "github.com/atomicals-go/atomicals-api/internal/types" 9 | "github.com/zeromicro/go-zero/rest/httpx" 10 | ) 11 | 12 | func getAssetByUserPkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | var req types.ReqAssetByUserPK 15 | if err := httpx.Parse(r, &req); err != nil { 16 | httpx.ErrorCtx(r.Context(), w, err) 17 | return 18 | } 19 | 20 | l := logic.NewGetAssetByUserPkLogic(r.Context(), svcCtx) 21 | resp, err := l.GetAssetByUserPk(&req) 22 | if err != nil { 23 | httpx.ErrorCtx(r.Context(), w, err) 24 | } else { 25 | httpx.OkJsonCtx(r.Context(), w, resp) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /atomicals-api/internal/handler/routes.go: -------------------------------------------------------------------------------- 1 | // Code generated by goctl. DO NOT EDIT. 2 | package handler 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/atomicals-go/atomicals-api/internal/svc" 8 | 9 | "github.com/zeromicro/go-zero/rest" 10 | ) 11 | 12 | func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { 13 | server.AddRoutes( 14 | []rest.Route{ 15 | { 16 | Method: http.MethodGet, 17 | Path: "/api/v1/checkTx", 18 | Handler: checkTxHandler(serverCtx), 19 | }, 20 | { 21 | Method: http.MethodGet, 22 | Path: "/api/v1/getAssetByLocationID", 23 | Handler: getAssetByLocationIDHandler(serverCtx), 24 | }, 25 | { 26 | Method: http.MethodGet, 27 | Path: "/api/v1/getAssetByUserPk", 28 | Handler: getAssetByUserPkHandler(serverCtx), 29 | }, 30 | }, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /atomicals-api/internal/logic/checktxlogic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/atomicals-go/atomicals-api/internal/svc" 8 | "github.com/atomicals-go/atomicals-api/internal/types" 9 | "github.com/atomicals-go/repo/postsql" 10 | 11 | "github.com/zeromicro/go-zero/core/logx" 12 | ) 13 | 14 | type CheckTxLogic struct { 15 | logx.Logger 16 | ctx context.Context 17 | svcCtx *svc.ServiceContext 18 | } 19 | 20 | func NewCheckTxLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckTxLogic { 21 | return &CheckTxLogic{ 22 | Logger: logx.WithContext(ctx), 23 | ctx: ctx, 24 | svcCtx: svcCtx, 25 | } 26 | } 27 | 28 | func (l *CheckTxLogic) CheckTx(req *types.ReqCheckTx) (resp *types.RespCheckTx, err error) { 29 | tx, height, err := l.svcCtx.GetTxByTxID(req.Txid) 30 | if err != nil { 31 | l.Errorf("[CheckTx] GetTxByTxID err:%v", err) 32 | return 33 | } 34 | if 0 <= height && height <= l.svcCtx.CurrentHeight { 35 | resp.Status = "confirmed" 36 | txRecord := &postsql.AtomicalsTx{} 37 | txRecord, err = l.svcCtx.AtomicalsTx(req.Txid) 38 | if err != nil { 39 | l.Errorf("[CheckTx] AtomicalsTx err:%v", err) 40 | return 41 | } 42 | resp.Operation = txRecord.Operation 43 | resp.Description = txRecord.Description 44 | } else if l.svcCtx.CurrentHeight < height && height < l.svcCtx.MaxBlockHeight { 45 | resp.Status = "until confirmation depth" 46 | data, ok := l.svcCtx.PendingAtomicalsAssetMap[req.Txid] 47 | if !ok { 48 | l.Errorf("[CheckTx] AtomicalsTx err:%v", errors.New("atomicals operation not found")) 49 | return 50 | } 51 | resp.Description = data.Description 52 | resp.Operation = data.Op 53 | } else if height < 0 { 54 | resp.Status = "in mempool" 55 | data := l.svcCtx.TraceTx(*tx, height) 56 | resp.Description = data.Description 57 | resp.Operation = data.Op 58 | } 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /atomicals-api/internal/logic/getassetbylocationidlogic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/atomicals-go/atomicals-api/internal/svc" 7 | "github.com/atomicals-go/atomicals-api/internal/types" 8 | 9 | "github.com/zeromicro/go-zero/core/logx" 10 | ) 11 | 12 | type GetassetByLocationIDLogic struct { 13 | logx.Logger 14 | ctx context.Context 15 | svcCtx *svc.ServiceContext 16 | } 17 | 18 | func NewGetassetByLocationIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetassetByLocationIDLogic { 19 | return &GetassetByLocationIDLogic{ 20 | Logger: logx.WithContext(ctx), 21 | ctx: ctx, 22 | svcCtx: svcCtx, 23 | } 24 | } 25 | 26 | func (l *GetassetByLocationIDLogic) GetAssetByLocationID(req *types.ReqAssetByLocationID) (resp *types.RespAssetByLocationID, err error) { 27 | entities, err := l.svcCtx.NftUTXOsByLocationID(req.LocationID) 28 | if err != nil { 29 | l.Errorf("[GetAssetByLocationID] NftUTXOsByLocationID err:%v", err) 30 | return 31 | } 32 | for _, v := range entities { 33 | resp.Assets = append(resp.Assets, &types.UTXONftInfo{ 34 | UserPk: v.UserPk, 35 | AtomicalsID: v.AtomicalsID, 36 | LocationID: v.LocationID, 37 | RealmName: v.RealmName, 38 | SubRealmName: v.SubRealmName, 39 | ParentRealmAtomicalsID: v.ParentRealmAtomicalsID, 40 | ContainerName: v.ContainerName, 41 | Dmitem: v.Dmitem, 42 | ParentContainerAtomicalsID: v.ParentContainerAtomicalsID, 43 | Time: v.Time, 44 | Bitworkc: v.Bitworkc, 45 | Bitworkr: v.Bitworkr, 46 | }) 47 | } 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /atomicals-api/internal/logic/getassetbyuserpklogic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/atomicals-go/atomicals-api/internal/svc" 7 | "github.com/atomicals-go/atomicals-api/internal/types" 8 | 9 | "github.com/zeromicro/go-zero/core/logx" 10 | ) 11 | 12 | type GetAssetByUserPkLogic struct { 13 | logx.Logger 14 | ctx context.Context 15 | svcCtx *svc.ServiceContext 16 | } 17 | 18 | func NewGetAssetByUserPkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAssetByUserPkLogic { 19 | return &GetAssetByUserPkLogic{ 20 | Logger: logx.WithContext(ctx), 21 | ctx: ctx, 22 | svcCtx: svcCtx, 23 | } 24 | } 25 | 26 | func (l *GetAssetByUserPkLogic) GetAssetByUserPk(req *types.ReqAssetByUserPK) (resp *types.RespAssetByUserPK, err error) { 27 | nfts, err := l.svcCtx.NftUTXOsByUserPK(req.UserPK) 28 | if err != nil { 29 | l.Errorf("[GetAssetByUserPk] NftUTXOsByUserPK err:%v", err) 30 | return 31 | } 32 | for _, v := range nfts { 33 | resp.NftAssets = append(resp.NftAssets, &types.UTXONftInfo{ 34 | UserPk: v.UserPk, 35 | AtomicalsID: v.AtomicalsID, 36 | LocationID: v.LocationID, 37 | RealmName: v.RealmName, 38 | SubRealmName: v.SubRealmName, 39 | ParentRealmAtomicalsID: v.ParentRealmAtomicalsID, 40 | ContainerName: v.ContainerName, 41 | Dmitem: v.Dmitem, 42 | ParentContainerAtomicalsID: v.ParentContainerAtomicalsID, 43 | Time: v.Time, 44 | Bitworkc: v.Bitworkc, 45 | Bitworkr: v.Bitworkr, 46 | }) 47 | } 48 | fts, err := l.svcCtx.FtUTXOsByUserPK(req.UserPK) 49 | if err != nil { 50 | l.Errorf("[GetAssetByUserPk] FtUTXOsByUserPK err:%v", err) 51 | return 52 | } 53 | for _, v := range fts { 54 | resp.FtAssets = append(resp.FtAssets, &types.UTXOFtInfo{ 55 | UserPk: v.UserPk, 56 | AtomicalsID: v.AtomicalsID, 57 | LocationID: v.LocationID, 58 | Bitworkc: v.Bitworkc, 59 | Bitworkr: v.Bitworkr, 60 | MintTicker: v.MintTicker, 61 | Time: v.Time, 62 | MintBitworkVec: v.MintBitworkVec, 63 | MintBitworkcInc: v.MintBitworkcInc, 64 | MintBitworkrInc: v.MintBitworkrInc, 65 | Amount: v.Amount, 66 | Type: v.Type, 67 | Subtype: v.Subtype, 68 | TickerName: v.TickerName, 69 | MaxSupply: v.MaxSupply, 70 | MintAmount: v.MintAmount, 71 | MintHeight: v.MintHeight, 72 | MaxMints: v.MaxMints, 73 | }) 74 | } 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /atomicals-api/internal/svc/servicecontext.go: -------------------------------------------------------------------------------- 1 | package svc 2 | 3 | import ( 4 | "github.com/atomicals-go/atomicals-api/internal/config" 5 | atomicals "github.com/atomicals-go/atomicals-indexer/atomicals-core/operation" 6 | "github.com/atomicals-go/pkg/conf" 7 | "github.com/atomicals-go/repo" 8 | ) 9 | 10 | type ServiceContext struct { 11 | Config config.Config 12 | *atomicals.Atomicals 13 | CurrentHeight int64 14 | MaxBlockHeight int64 15 | PendingAtomicalsAssetMap map[string]*repo.AtomicaslData // key: txID 16 | } 17 | 18 | func NewServiceContext(c config.Config, atomicalsConfigFilePath string) *ServiceContext { 19 | conf, err := conf.ReadJSONFromJSFile(atomicalsConfigFilePath) 20 | if err != nil { 21 | panic(err) 22 | } 23 | a := atomicals.NewAtomicalsWithSQL(conf) 24 | svc := &ServiceContext{ 25 | Config: c, 26 | Atomicals: a, 27 | PendingAtomicalsAssetMap: make(map[string]*repo.AtomicaslData, 0), 28 | } 29 | return svc 30 | } 31 | -------------------------------------------------------------------------------- /atomicals-api/internal/svc/syncAtomicalsAsset.go: -------------------------------------------------------------------------------- 1 | package svc 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/atomicals-go/repo" 8 | "github.com/atomicals-go/utils" 9 | ) 10 | 11 | func (a *ServiceContext) SyncPendingAtomicalsAsset() { 12 | location, err := a.Location() 13 | if err != nil { 14 | return 15 | } 16 | if a.CurrentHeight == location.BlockHeight { 17 | time.Sleep(10 * time.Minute) 18 | return 19 | } 20 | maxBlockHeight, err := a.GetBlockCount() 21 | if err != nil { 22 | return 23 | } 24 | if location.BlockHeight < maxBlockHeight-utils.SafeBlockHeightInterupt { 25 | panic(fmt.Sprintf("waiting for atomicals-core sync to:%v, current height:%v", maxBlockHeight-utils.SafeBlockHeightInterupt, location.BlockHeight)) 26 | } 27 | pendingAtomicalsAssetMap := make(map[string]*repo.AtomicaslData, 0) 28 | for height := int64(location.BlockHeight + 1); height <= maxBlockHeight; height++ { 29 | block, err := a.GetBlockByHeight(height) 30 | if err != nil { 31 | return 32 | } 33 | for _, tx := range block.Tx { 34 | data := a.TraceTx(tx, height) 35 | pendingAtomicalsAssetMap[tx.Txid] = data 36 | } 37 | } 38 | a.PendingAtomicalsAssetMap = pendingAtomicalsAssetMap 39 | a.CurrentHeight = location.BlockHeight 40 | a.MaxBlockHeight = maxBlockHeight 41 | } 42 | -------------------------------------------------------------------------------- /atomicals-api/internal/types/types.go: -------------------------------------------------------------------------------- 1 | // Code generated by goctl. DO NOT EDIT. 2 | package types 3 | 4 | type ReqAssetByLocationID struct { 5 | LocationID string `json:"location_id"` 6 | } 7 | 8 | type ReqAssetByUserPK struct { 9 | UserPK string `json:"user_pk"` 10 | } 11 | 12 | type ReqCheckTx struct { 13 | Txid string `json:"tx_id"` 14 | } 15 | 16 | type RespAssetByLocationID struct { 17 | Assets []*UTXONftInfo `json:"assets"` 18 | } 19 | 20 | type RespAssetByUserPK struct { 21 | NftAssets []*UTXONftInfo `json:"nft_assets"` 22 | FtAssets []*UTXOFtInfo `json:"ft_assets"` 23 | } 24 | 25 | type RespCheckTx struct { 26 | Operation string `json:"operation"` 27 | Description string `json:"description"` 28 | Status string `json:"status"` 29 | } 30 | 31 | type UTXONftInfo struct { 32 | UserPk string `json:"user_pk"` 33 | AtomicalsID string `json:"atomicals_id"` 34 | LocationID string `json:"location_id"` 35 | RealmName string `json:"realm_name"` 36 | SubRealmName string `json:"subrealm_name"` 37 | ParentRealmAtomicalsID string `json:"parent_realm_atomicals_id"` 38 | ContainerName string `json:"container_name"` 39 | Dmitem string `json:"dmitem"` 40 | ParentContainerAtomicalsID string `json:"parent_container_atomicals_id"` 41 | Time int64 `json:"time"` 42 | Bitworkc string `json:"bitworkc"` 43 | Bitworkr string `json:"bitworkr"` 44 | } 45 | 46 | type UTXOFtInfo struct { 47 | UserPk string `json:"user_pk"` 48 | AtomicalsID string `json:"atomicals_id"` 49 | LocationID string `json:"location_id"` 50 | 51 | Bitworkc string `json:"bitworkc"` 52 | Bitworkr string `json:"bitworkr"` 53 | 54 | // DistributedFt 55 | MintTicker string `json:"mint_ticker"` 56 | Time int64 `json:"time"` 57 | MintBitworkVec string `json:"mint_bitwork_vec"` 58 | MintBitworkcInc string `json:"mint_bitworkc_inc"` 59 | MintBitworkrInc string `json:"mint_bitwork_rinc"` 60 | Amount int64 `json:"amount"` 61 | 62 | // DirectFt 63 | Type string `json:"type"` 64 | Subtype string `json:"sub_type"` 65 | TickerName string `json:"ticker_name"` 66 | MaxSupply int64 `json:"max_supply"` 67 | MintAmount int64 `json:"mint_amount"` 68 | MintHeight int64 `json:"mint_height"` 69 | MaxMints int64 `json:"max_mints"` 70 | } 71 | 72 | -------------------------------------------------------------------------------- /atomicals-api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/atomicals-go/atomicals-api/internal/config" 8 | "github.com/atomicals-go/atomicals-api/internal/handler" 9 | "github.com/atomicals-go/atomicals-api/internal/svc" 10 | 11 | "github.com/zeromicro/go-zero/core/conf" 12 | "github.com/zeromicro/go-zero/rest" 13 | ) 14 | 15 | var configFile = flag.String("f", "etc/main-api.yaml", "the config file") 16 | var atomicalsConfigFilePath = "etc/config.json" 17 | 18 | func main() { 19 | flag.Parse() 20 | 21 | var c config.Config 22 | conf.MustLoad(*configFile, &c) 23 | 24 | server := rest.MustNewServer(c.RestConf) 25 | defer server.Stop() 26 | 27 | ctx := svc.NewServiceContext(c, atomicalsConfigFilePath) 28 | go ctx.SyncPendingAtomicalsAsset() 29 | 30 | handler.RegisterHandlers(server, ctx) 31 | 32 | fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) 33 | server.Start() 34 | } 35 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/atomicals.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/atomicals-go/pkg/btcsync" 7 | "github.com/atomicals-go/pkg/conf" 8 | "github.com/atomicals-go/pkg/log" 9 | "github.com/atomicals-go/repo" 10 | "github.com/atomicals-go/repo/postsql" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type Atomicals struct { 15 | *btcsync.BtcSync 16 | repo.DB 17 | location *postsql.Location 18 | maxBlockHeight int64 19 | SyncTxHeightMap sync.Map // map[string]int64 20 | } 21 | 22 | func NewAtomicalsWithSQL(conf *conf.Config) *Atomicals { 23 | db := repo.NewSqlDB(conf.SqlDNS) 24 | location, err := db.Location() 25 | if err != nil { 26 | if err == gorm.ErrRecordNotFound { 27 | location = &postsql.Location{ 28 | Key: postsql.LocationKey, 29 | BlockHeight: conf.AtomicalsStartHeight, 30 | TxIndex: -1, 31 | } 32 | } else { 33 | log.Log.Panicf("Location err:%v", err) 34 | } 35 | } 36 | btcsync, err := btcsync.NewBtcSync(conf.BtcRpcURL, conf.BtcRpcUser, conf.BtcRpcPassword) 37 | if err != nil { 38 | panic(err) 39 | } 40 | maxBlockHeight, err := btcsync.GetBlockCount() 41 | if err != nil { 42 | log.Log.Panicf("GetBlockCount err:%v", err) 43 | } 44 | a := &Atomicals{ 45 | DB: db, 46 | BtcSync: btcsync, 47 | location: location, 48 | maxBlockHeight: maxBlockHeight, 49 | } 50 | return a 51 | } 52 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/merkleVerify.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 7 | "github.com/atomicals-go/pkg/log" 8 | "github.com/atomicals-go/pkg/merkle" 9 | "github.com/atomicals-go/utils" 10 | ) 11 | 12 | func (m *Atomicals) verifyRuleAndMerkle(operation *witness.WitnessAtomicalsOperation) bool { 13 | // get_dmitem_parent_container_info 14 | dmintValidatedStatus, err := m.getModHistory(operation.Payload.Args.ParentContainer, operation.RevealLocationHeight) 15 | if err != nil { 16 | panic(err) 17 | } 18 | if operation.CommitHeight < dmintValidatedStatus.MintHeight || operation.RevealLocationHeight < dmintValidatedStatus.MintHeight { 19 | return false 20 | } 21 | parentContainer, err := m.NftUTXOByAtomicalsID(operation.Payload.Args.ParentContainer) 22 | if err != nil { 23 | log.Log.Panicf("NftUTXOByAtomicalsID err:%v", err) 24 | } 25 | latestItem, err := m.LatestItemByContainerName(parentContainer.ContainerName) 26 | if err != nil { 27 | log.Log.Panicf("LatestItemByContainerName err:%v", err) 28 | } 29 | txID, _ := utils.SplitAtomicalsID(latestItem.LocationID) 30 | latsetMintHeight, err := m.AtomicalsTxHeight(txID) 31 | if err != nil { 32 | log.Log.Panicf("AtomicalsTxHeight err:%v", err) 33 | } 34 | if operation.CommitHeight < latsetMintHeight { 35 | return false 36 | } 37 | if operation.RevealLocationHeight < latsetMintHeight { 38 | return false 39 | } 40 | // validate_dmitem_mint_args_with_container_dmint 41 | for _, proof_item := range operation.Payload.Args.Proof { 42 | if len(proof_item.D) != 64 { 43 | return false 44 | } 45 | } 46 | image := operation.Payload.Args.DynamicFields[operation.Payload.Args.Main] 47 | is_proof_valid, err := validateMerkleProofDmint(dmintValidatedStatus.Merkle, 48 | operation.Payload.Args.RequestDmitem, operation.Payload.Args.Bitworkc, 49 | operation.Payload.Args.Bitworkr, operation.Payload.Args.Main, utils.DoubleSha256(image), operation.Payload.Args.Proof) 50 | if err != nil { 51 | return false 52 | } 53 | if !is_proof_valid { 54 | return false 55 | } 56 | matchedPricePoint := m.get_applicable_rule_by_height(operation.Payload.Args.ParentContainer, operation.Payload.Args.RequestDmitem, operation.RevealLocationHeight) 57 | return m.checkRule(matchedPricePoint, operation.Payload.Args.Bitworkc, operation.Payload.Args.Bitworkr) 58 | } 59 | 60 | func validateMerkleProofDmint(merkleStr string, item_name string, possible_bitworkc, possible_bitworkr, main string, main_hash []byte, proof []witness.Proof) (bool, error) { 61 | expected_root_hash, err := hex.DecodeString(merkleStr) 62 | if err != nil { 63 | return false, err 64 | } 65 | // # Case 1: any/any 66 | concat_str1 := item_name + ":any" + ":any:" + main + ":" + hex.EncodeToString(main_hash) 67 | target_hash := utils.Sha256([]byte(concat_str1)) 68 | // log.Log.Panicf("UpdateCurrentHeight err:%v", expected_root_hash1) 69 | 70 | if merkle.CheckValidateProof(expected_root_hash, target_hash, proof) { 71 | return true, nil 72 | } 73 | // # Case 2: specific_bitworkc/any 74 | if possible_bitworkc != "" { 75 | concat_str2 := item_name + ":" + possible_bitworkc + ":any:" + main + ":" + hex.EncodeToString(main_hash) 76 | target_hash := utils.Sha256([]byte(concat_str2)) 77 | if merkle.CheckValidateProof(expected_root_hash, target_hash, proof) { 78 | return true, nil 79 | } 80 | } 81 | // # Case 3: any/specific_bitworkr 82 | if possible_bitworkr != "" { 83 | concat_str3 := item_name + ":any" + ":" + possible_bitworkr + ":" + main + ":" + hex.EncodeToString(main_hash) 84 | target_hash := utils.Sha256([]byte(concat_str3)) 85 | if merkle.CheckValidateProof(expected_root_hash, target_hash, proof) { 86 | return true, nil 87 | } 88 | } 89 | // # Case 4: 90 | if possible_bitworkc != "" && possible_bitworkr != "" { 91 | concat_str4 := item_name + ":" + possible_bitworkc + ":" + possible_bitworkr + ":" + main + ":" + hex.EncodeToString(main_hash) 92 | target_hash := utils.Sha256([]byte(concat_str4)) 93 | if merkle.CheckValidateProof(expected_root_hash, target_hash, proof) { 94 | return true, nil 95 | } 96 | } 97 | return false, nil 98 | } 99 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/operationDat.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 5 | "github.com/atomicals-go/repo/postsql" 6 | "github.com/btcsuite/btcd/btcjson" 7 | ) 8 | 9 | func (m *Atomicals) operationDat(operation *witness.WitnessAtomicalsOperation, tx btcjson.TxRawResult) *postsql.DatInfo { 10 | return &postsql.DatInfo{ 11 | Height: operation.RevealLocationHeight, 12 | AtomicalsID: operation.AtomicalsID, 13 | LocationID: operation.LocationID, 14 | // Dat: operation.PayloadStr, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/operationDft.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 7 | "github.com/atomicals-go/pkg/errors" 8 | "github.com/atomicals-go/pkg/log" 9 | "github.com/atomicals-go/repo/postsql" 10 | "github.com/atomicals-go/utils" 11 | ) 12 | 13 | // deployDistributedFt: operation dft 14 | func (m *Atomicals) deployDistributedFt(operation *witness.WitnessAtomicalsOperation, userPk string) (newGlobalDistributedFt *postsql.GlobalDistributedFt, err error) { 15 | if operation.RevealInputIndex != 0 { 16 | return nil, errors.ErrInvalidRevealInputIndex 17 | } 18 | if !operation.Payload.CheckRequest() { 19 | return nil, errors.ErrCheckRequest 20 | } 21 | if !utils.IsValidTicker(operation.Payload.Args.RequestTicker) { 22 | return nil, errors.ErrInvalidTicker 23 | } 24 | ft, err := m.DistributedFtByName(operation.Payload.Args.RequestTicker) 25 | if err != nil { 26 | log.Log.Panicf("DistributedFtByName err:%v", err) 27 | } 28 | if ft != nil { 29 | return nil, errors.ErrTickerHasExist 30 | } 31 | if operation.Payload.Args.Bitworkc == "" { 32 | return nil, errors.ErrBitworkcNeeded 33 | } 34 | _, _, err = operation.IsValidBitwork() 35 | if err != nil { 36 | return nil, err 37 | } 38 | if operation.Payload.Args.MintHeight < utils.DFT_MINT_HEIGHT_MIN || utils.DFT_MINT_HEIGHT_MAX < operation.Payload.Args.MintHeight { 39 | return nil, errors.ErrInvalidMintHeight 40 | } 41 | if operation.Payload.Args.MintAmount < utils.DFT_MINT_AMOUNT_MIN || utils.DFT_MINT_AMOUNT_MAX < operation.Payload.Args.MintAmount { 42 | return nil, errors.ErrInvalidMintHeight 43 | } 44 | if operation.Payload.Args.MaxMints < utils.DFT_MINT_MAX_MIN_COUNT { 45 | return nil, errors.ErrInvalidMaxMints 46 | } 47 | if operation.RevealLocationHeight < utils.ATOMICALS_ACTIVATION_HEIGHT_DENSITY { 48 | if operation.Payload.Args.MaxMints > utils.DFT_MINT_MAX_MAX_COUNT_LEGACY { 49 | return nil, errors.ErrInvalidMaxMints 50 | } 51 | } else { 52 | if operation.Payload.Args.MaxMints > utils.DFT_MINT_MAX_MAX_COUNT_DENSITY { 53 | return nil, errors.ErrInvalidMaxMints 54 | } 55 | } 56 | mintBitworkc, _, err := utils.ParseMintBitwork(operation.CommitTxID, operation.Payload.Args.MintBitworkc, operation.Payload.Args.MintBitworkr) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if mintBitworkc != nil && len(mintBitworkc.Prefix) < 4 { 61 | return nil, errors.ErrInvalidBitworkcPrefix 62 | } 63 | if operation.Payload.IsImmutable() { 64 | return nil, errors.ErrCannotBeImmutable 65 | } 66 | if operation.Payload.Args.Md != "" && operation.Payload.Args.Md != "0" && operation.Payload.Args.Md != "1" { 67 | return nil, errors.ErrInvalidDftMd 68 | } 69 | if operation.RevealLocationHeight > utils.ATOMICALS_ACTIVATION_HEIGHT_COMMITZ && operation.CommitVoutIndex != utils.VOUT_EXPECT_OUTPUT_INDEX { 70 | return nil, errors.ErrInvalidVinIndex 71 | } 72 | operation.CommitHeight, err = m.GetTxHeightByTxID(operation.CommitTxID) 73 | if err != nil { 74 | panic(err) 75 | } 76 | if operation.CommitHeight < utils.ATOMICALS_ACTIVATION_HEIGHT { 77 | return nil, errors.ErrInvalidCommitHeight 78 | } 79 | if !operation.IsWithinAcceptableBlocksForGeneralReveal() { 80 | return nil, errors.ErrInvalidCommitHeight 81 | } 82 | if !operation.IsWithinAcceptableBlocksForNameReveal() { 83 | return nil, errors.ErrInvalidCommitHeight 84 | } 85 | newGlobalDistributedFt = &postsql.GlobalDistributedFt{ 86 | AtomicalsID: operation.AtomicalsID, 87 | LocationID: operation.LocationID, 88 | TickerName: operation.Payload.Args.RequestTicker, 89 | Type: "FT", 90 | Subtype: "decentralized", 91 | MintAmount: operation.Payload.Args.MintAmount, 92 | MaxMints: operation.Payload.Args.MaxMints, 93 | MintHeight: operation.Payload.Args.MintHeight, 94 | MintBitworkc: operation.Payload.Args.MintBitworkc, 95 | MintBitworkr: operation.Payload.Args.MintBitworkr, 96 | Bitworkc: operation.Payload.Args.Bitworkc, 97 | Bitworkr: operation.Payload.Args.Bitworkr, 98 | // Meta: operation.Payload.Meta, 99 | MintedTimes: 0, 100 | Md: operation.Payload.Args.Md, 101 | Bv: operation.Payload.Args.Bv, 102 | Bci: operation.Payload.Args.Bci, 103 | Bri: operation.Payload.Args.Bri, 104 | Bcs: operation.Payload.Args.Bcs, 105 | Brs: operation.Payload.Args.Brs, 106 | Maxg: operation.Payload.Args.Maxg, 107 | CommitHeight: operation.CommitHeight, 108 | } 109 | 110 | if utils.ATOMICALS_ACTIVATION_HEIGHT_DENSITY <= operation.RevealLocationHeight && newGlobalDistributedFt.Md == "1" { 111 | if !utils.IsHexStringRegex(operation.Payload.Args.Bv) || len(operation.Payload.Args.Bv) < 4 { 112 | return nil, errors.ErrInvalidDftBv 113 | } 114 | if operation.Payload.Args.MintBitworkc != "" || operation.Payload.Args.MintBitworkr != "" { 115 | return nil, errors.ErrInvalidDftMintBitwork 116 | } 117 | if operation.Payload.Args.Bci != "" { 118 | bci, err := strconv.Atoi(operation.Payload.Args.Bci) 119 | if err == nil { 120 | if 64 < bci { 121 | return nil, errors.ErrInvalidDftBci 122 | } 123 | if operation.Payload.Args.Bcs < 64 || 256 < operation.Payload.Args.Bcs { 124 | return nil, errors.ErrInvalidDftBsc 125 | } 126 | } 127 | } 128 | if operation.Payload.Args.Bri != "" { 129 | bri, err := strconv.Atoi(operation.Payload.Args.Bri) 130 | if err == nil { 131 | if 64 < bri { 132 | return nil, errors.ErrInvalidDftBri 133 | } 134 | if operation.Payload.Args.Brs < 64 || 256 < operation.Payload.Args.Brs { 135 | return nil, errors.ErrInvalidDftBrs 136 | } 137 | } 138 | } 139 | if 100000 < operation.Payload.Args.MaxMints { 140 | return nil, errors.ErrInvalidMaxMints 141 | } 142 | if operation.Payload.Args.Maxg < utils.DFT_MINT_MAX_MIN_COUNT || utils.DFT_MINT_MAX_MAX_COUNT_DENSITY < operation.Payload.Args.Maxg { 143 | return nil, errors.ErrInvalidDftMaxg 144 | } 145 | newGlobalDistributedFt.MaxMintsGlobal = operation.Payload.Args.Maxg 146 | newGlobalDistributedFt.MintMode = "perpetual" 147 | if newGlobalDistributedFt.MaxMintsGlobal != 0 { 148 | newGlobalDistributedFt.MaxSupply = newGlobalDistributedFt.MintAmount * newGlobalDistributedFt.MaxMintsGlobal 149 | } else { 150 | newGlobalDistributedFt.MaxSupply = -1 151 | } 152 | } else { 153 | newGlobalDistributedFt.MintMode = "fixed" 154 | newGlobalDistributedFt.MaxSupply = newGlobalDistributedFt.MintAmount * newGlobalDistributedFt.MaxMints 155 | } 156 | return newGlobalDistributedFt, nil 157 | } 158 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/operationDmt.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 7 | "github.com/atomicals-go/pkg/errors" 8 | "github.com/atomicals-go/pkg/log" 9 | "github.com/atomicals-go/repo/postsql" 10 | "github.com/atomicals-go/utils" 11 | "github.com/btcsuite/btcd/btcjson" 12 | ) 13 | 14 | // mintDistributedFt:operation dmt, Mint tokens of distributed mint type 15 | func (m *Atomicals) mintDistributedFt(operation *witness.WitnessAtomicalsOperation, vout []btcjson.Vout, userPk string) (newUTXOFtInfo *postsql.UTXOFtInfo, updateDistributedFt *postsql.GlobalDistributedFt, err error) { 16 | if operation.RevealInputIndex != 0 { 17 | return nil, nil, errors.ErrInvalidRevealInputIndex 18 | } 19 | ticker := operation.Payload.Args.MintTicker 20 | updateDistributedFt, err = m.DistributedFtByName(ticker) 21 | if err != nil { 22 | log.Log.Panicf("DistributedFtByName err:%v", err) 23 | } 24 | if updateDistributedFt == nil { 25 | return nil, nil, errors.ErrNotDeployFt 26 | } 27 | if operation.RevealLocationHeight < updateDistributedFt.CommitHeight+utils.MINT_REALM_CONTAINER_TICKER_COMMIT_REVEAL_DELAY_BLOCKS { 28 | return nil, nil, errors.ErrInvalidCommitHeight 29 | } 30 | if operation.RevealLocationHeight >= utils.ATOMICALS_ACTIVATION_HEIGHT_COMMITZ && operation.CommitVoutIndex != utils.VOUT_EXPECT_OUTPUT_INDEX { 31 | return nil, nil, errors.ErrInvalidVinIndex 32 | } 33 | // if mint_amount == txout.value: 34 | amount := utils.MulSatoshi(vout[utils.VOUT_EXPECT_OUTPUT_INDEX].Value) 35 | if amount != updateDistributedFt.MintAmount { 36 | return nil, nil, errors.ErrInvalidMintAmount 37 | } 38 | 39 | if updateDistributedFt.MintMode == "perpetual" { 40 | if updateDistributedFt.MaxMintsGlobal == updateDistributedFt.MintedTimes { 41 | return nil, nil, nil 42 | } 43 | if updateDistributedFt.Bci != "" { 44 | if operation.IsDftBitworkRolloverActivated() { 45 | success, _ := isTxidValidForPerpetualBitwork(operation.CommitTxID, updateDistributedFt.Bv, updateDistributedFt.MintedTimes, updateDistributedFt.MaxMints, updateDistributedFt.Bci, updateDistributedFt.Bcs, true) 46 | if !success { 47 | return nil, nil, errors.ErrInvalidPerpetualBitwork 48 | } 49 | } else { 50 | success, _ := isTxidValidForPerpetualBitwork(operation.CommitTxID, updateDistributedFt.Bv, updateDistributedFt.MintedTimes, updateDistributedFt.MaxMints, updateDistributedFt.Bci, updateDistributedFt.Bcs, false) 51 | if !success { 52 | return nil, nil, errors.ErrInvalidPerpetualBitwork 53 | } 54 | } 55 | } 56 | if updateDistributedFt.Bri != "" { 57 | if operation.IsDftBitworkRolloverActivated() { 58 | success, _ := isTxidValidForPerpetualBitwork(operation.RevealLocationTxID, updateDistributedFt.Bv, updateDistributedFt.MintedTimes, updateDistributedFt.MaxMints, updateDistributedFt.Bri, updateDistributedFt.Brs, true) 59 | if !success { 60 | return nil, nil, errors.ErrInvalidPerpetualBitwork 61 | } 62 | } else { 63 | success, _ := isTxidValidForPerpetualBitwork(operation.RevealLocationTxID, updateDistributedFt.Bv, updateDistributedFt.MintedTimes, updateDistributedFt.MaxMints, updateDistributedFt.Bri, updateDistributedFt.Brs, false) 64 | if !success { 65 | return nil, nil, errors.ErrInvalidPerpetualBitwork 66 | } 67 | } 68 | } 69 | } else { //updateDistributedFt.MintMode == "fixed" 70 | if updateDistributedFt.MintedTimes >= updateDistributedFt.MaxMints { 71 | return nil, nil, errors.ErrInvalidMintedTimes 72 | } else if updateDistributedFt.MintedTimes < updateDistributedFt.MaxMints { 73 | bitworkc, bitworkr, err := utils.ParseMintBitwork(operation.CommitTxID, operation.Payload.Args.MintBitworkc, operation.Payload.Args.MintBitworkr) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | if bitworkc != nil { 78 | if !utils.IsProofOfWorkPrefixMatch(operation.CommitTxID, bitworkc.Prefix, bitworkc.Ext) { 79 | return nil, nil, errors.ErrInvalidBitWork 80 | } 81 | } 82 | if bitworkr != nil { 83 | if !utils.IsProofOfWorkPrefixMatch(operation.CommitTxID, bitworkr.Prefix, bitworkr.Ext) { 84 | return nil, nil, errors.ErrInvalidBitWork 85 | } 86 | } 87 | } 88 | } 89 | _, _, err = operation.IsValidBitwork() 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | operation.CommitHeight = m.getTxHeight(operation.CommitTxID) 94 | if operation.CommitHeight < updateDistributedFt.MintHeight { 95 | return nil, nil, errors.ErrInvalidCommitHeight 96 | } 97 | newUTXOFtInfo = &postsql.UTXOFtInfo{ 98 | UserPk: userPk, 99 | MintTicker: ticker, 100 | Time: operation.Payload.Args.Time, 101 | Bitworkc: operation.Payload.Args.Bitworkc, 102 | Bitworkr: operation.Payload.Args.Bitworkr, 103 | Amount: amount, 104 | AtomicalsID: updateDistributedFt.AtomicalsID, 105 | LocationID: operation.LocationID, 106 | } 107 | updateDistributedFt.MintedTimes = updateDistributedFt.MintedTimes + 1 108 | return newUTXOFtInfo, updateDistributedFt, nil 109 | } 110 | 111 | // is_txid_valid_for_perpetual_bitwork 112 | func isTxidValidForPerpetualBitwork(txid string, bitworkVec string, actualMints, maxMints int64, mintBitworkrInc string, mintBitworkcStart int64, allowHigher bool) (bool, string) { 113 | startingTarget := mintBitworkcStart 114 | targetIncrement, _ := strconv.Atoi(mintBitworkrInc) // never return err 115 | expectedMinimumBitwork := utils.Calculate_expected_bitwork(bitworkVec, actualMints, maxMints, int64(targetIncrement), startingTarget) 116 | if utils.IsMintPowValid(txid, expectedMinimumBitwork) { 117 | return true, expectedMinimumBitwork 118 | } 119 | if allowHigher { 120 | parts := utils.ParseBitwork(expectedMinimumBitwork) 121 | if parts == nil { 122 | return false, "" 123 | } 124 | prefix := parts.Prefix 125 | nextFullBitworkPrefix := utils.GetNextBitworkFullStr(bitworkVec, len(prefix)) 126 | if utils.IsMintPowValid(txid, nextFullBitworkPrefix) { 127 | return true, nextFullBitworkPrefix 128 | } 129 | } 130 | return false, "" 131 | } 132 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/operationFt.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 5 | "github.com/atomicals-go/pkg/errors" 6 | "github.com/atomicals-go/pkg/log" 7 | "github.com/atomicals-go/repo/postsql" 8 | "github.com/atomicals-go/utils" 9 | "github.com/btcsuite/btcd/btcjson" 10 | ) 11 | 12 | // mintDirectFt: Mint fungible token with direct fixed supply 13 | func (m *Atomicals) mintDirectFt(operation *witness.WitnessAtomicalsOperation, vout []btcjson.Vout, userPk string) (newGlobalDirectFt *postsql.GlobalDirectFt, err error) { 14 | if operation.RevealInputIndex != 0 { 15 | return nil, errors.ErrInvalidRevealInputIndex 16 | } 17 | if !operation.Payload.CheckRequest() { 18 | return nil, errors.ErrCheckRequest 19 | } 20 | if !utils.IsValidTicker(operation.Payload.Args.RequestTicker) { 21 | return nil, errors.ErrInvalidTicker 22 | } 23 | ft, err := m.DirectFtByName(operation.Payload.Args.RequestTicker) 24 | if err != nil { 25 | log.Log.Panicf("DistributedFtByName err:%v", err) 26 | } 27 | if ft != nil { 28 | return nil, errors.ErrTickerHasExist 29 | } 30 | if operation.Payload.IsImmutable() { 31 | return nil, errors.ErrCannotBeImmutable 32 | } 33 | if operation.Payload.Args.Bitworkc == "" { 34 | return nil, errors.ErrBitworkcNeeded 35 | } 36 | _, _, err = operation.IsValidBitwork() 37 | if err != nil { 38 | return nil, err 39 | } 40 | operation.CommitHeight = m.getTxHeight(operation.CommitTxID) 41 | if operation.CommitHeight < utils.ATOMICALS_ACTIVATION_HEIGHT { 42 | return nil, errors.ErrInvalidCommitHeight 43 | } 44 | if !operation.IsWithinAcceptableBlocksForGeneralReveal() { 45 | return nil, errors.ErrInvalidCommitHeight 46 | } 47 | if !operation.IsWithinAcceptableBlocksForNameReveal() { 48 | return nil, errors.ErrInvalidCommitHeight 49 | } 50 | if operation.RevealLocationHeight > utils.ATOMICALS_ACTIVATION_HEIGHT_COMMITZ && operation.CommitVoutIndex != utils.VOUT_EXPECT_OUTPUT_INDEX { 51 | return nil, errors.ErrInvalidVinIndex 52 | } 53 | if operation.CommitVoutIndex != utils.VOUT_EXPECT_OUTPUT_INDEX { 54 | return nil, errors.ErrInvalidVinIndex 55 | } 56 | 57 | amount := utils.MulSatoshi(vout[utils.VOUT_EXPECT_OUTPUT_INDEX].Value) 58 | newGlobalDirectFt = &postsql.GlobalDirectFt{ 59 | UserPk: userPk, 60 | AtomicalsID: operation.AtomicalsID, 61 | LocationID: operation.LocationID, 62 | Type: "FT", 63 | Subtype: "direct", 64 | TickerName: operation.Payload.Args.RequestTicker, 65 | // Meta: operation.Payload.Meta, 66 | Bitworkc: operation.Payload.Args.Bitworkc, 67 | Bitworkr: operation.Payload.Args.Bitworkr, 68 | MaxSupply: amount, 69 | } 70 | return newGlobalDirectFt, nil 71 | } 72 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/operationMod.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 7 | "github.com/atomicals-go/pkg/log" 8 | "github.com/atomicals-go/repo/postsql" 9 | "github.com/atomicals-go/utils" 10 | "github.com/btcsuite/btcd/btcjson" 11 | ) 12 | 13 | func (m *Atomicals) operationMod(operation *witness.WitnessAtomicalsOperation, tx btcjson.TxRawResult) *postsql.ModInfo { 14 | if operation.Op != "mod" || len(tx.Vin) == 0 { 15 | return nil 16 | } 17 | var preNfts []*postsql.UTXONftInfo 18 | var err error 19 | for _, vin := range tx.Vin { 20 | preNftLocationID := utils.AtomicalsID(vin.Txid, int64(vin.Vout)) 21 | preNfts, err = m.NftUTXOsByLocationID(preNftLocationID) 22 | if err != nil { 23 | log.Log.Panicf("NftUTXOsByLocationID err:%v", err) 24 | } 25 | } 26 | if len(preNfts) == 0 { 27 | return nil 28 | } 29 | r, err := json.Marshal(operation.Payload.Dmint) 30 | if err != nil { 31 | log.Log.Panicf("Marshal err:%v", err) 32 | } 33 | return &postsql.ModInfo{ 34 | Height: operation.RevealLocationHeight, 35 | AtomicalsID: preNfts[0].AtomicalsID, 36 | LocationID: preNfts[0].LocationID, 37 | Mod: string(r), 38 | // ModStr: operation.PayloadStr, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/operationNft.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 5 | "github.com/atomicals-go/pkg/errors" 6 | "github.com/atomicals-go/pkg/log" 7 | "github.com/atomicals-go/repo/postsql" 8 | "github.com/atomicals-go/utils" 9 | ) 10 | 11 | func (m *Atomicals) mintNft(operation *witness.WitnessAtomicalsOperation, userPk string) (newUTXONftInfo *postsql.UTXONftInfo, deleteUTXONfts []*postsql.UTXONftInfo, err error) { 12 | if operation.RevealInputIndex != 0 { 13 | return nil, nil, errors.ErrInvalidRevealInputIndex 14 | } 15 | if !operation.Payload.CheckRequest() { 16 | return nil, nil, errors.ErrCheckRequest 17 | } 18 | operation.CommitHeight = m.getTxHeight(operation.CommitTxID) 19 | if operation.CommitHeight < utils.ATOMICALS_ACTIVATION_HEIGHT { 20 | return nil, nil, errors.ErrInvalidCommitHeight 21 | } 22 | if !operation.IsWithinAcceptableBlocksForGeneralReveal() { 23 | return nil, nil, errors.ErrInvalidCommitHeight 24 | } 25 | if !operation.IsWithinAcceptableBlocksForNameReveal() { 26 | return nil, nil, errors.ErrInvalidCommitHeight 27 | } 28 | if operation.RevealLocationHeight >= utils.ATOMICALS_ACTIVATION_HEIGHT_COMMITZ && operation.CommitVoutIndex != utils.VOUT_EXPECT_OUTPUT_INDEX { 29 | return nil, nil, errors.ErrInvalidVinIndex 30 | } 31 | if operation.Payload.Args.RequestRealm != "" { 32 | if !utils.IsValidRealm(operation.Payload.Args.RequestRealm) { 33 | return nil, nil, errors.ErrInvalidRealm 34 | } 35 | b, err := m.GetBlockByHeight(operation.CommitHeight) 36 | if err != nil { 37 | log.Log.Panicf("GetBlockByHeight err:%v", err) 38 | } 39 | for txIndex, tx := range b.Tx { 40 | if tx.Txid == operation.CommitTxID { 41 | operation.CommitTxIndex = int64(txIndex) 42 | } 43 | } 44 | if operation.Payload.IsImmutable() { 45 | return nil, nil, errors.ErrCannotBeImmutable 46 | } 47 | if operation.Payload.Args.Bitworkc == "" { 48 | return nil, nil, errors.ErrBitworkcNeeded 49 | } 50 | bitworkc, _, err := operation.IsValidBitwork() 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | if bitworkc != nil && len(bitworkc.Prefix) < 4 { 55 | return nil, nil, errors.ErrInvalidBitworkcPrefix 56 | } 57 | realms, err := m.NftRealmByName(operation.Payload.Args.RequestRealm) 58 | if err != nil { 59 | log.Log.Panicf("NftRealmByName err:%v", err) 60 | } 61 | if len(realms) != 0 { 62 | for _, v := range realms { 63 | if v.CommitHeight < operation.CommitHeight { 64 | return nil, nil, errors.ErrRealmHasExist 65 | } else if v.CommitHeight == operation.CommitHeight { 66 | if v.CommitTxIndex < operation.CommitTxIndex { 67 | return nil, nil, errors.ErrRealmHasExist 68 | } else { 69 | deleteUTXONfts = append(deleteUTXONfts, realms...) 70 | } 71 | } else { 72 | deleteUTXONfts = append(deleteUTXONfts, realms...) 73 | } 74 | } 75 | } 76 | newUTXONftInfo = &postsql.UTXONftInfo{ 77 | UserPk: userPk, 78 | RealmName: operation.Payload.Args.RequestRealm, 79 | Time: operation.Payload.Args.Time, 80 | Bitworkc: operation.Payload.Args.Bitworkc, 81 | Bitworkr: operation.Payload.Args.Bitworkr, 82 | AtomicalsID: operation.AtomicalsID, 83 | CommitHeight: operation.CommitHeight, 84 | CommitTxIndex: operation.CommitTxIndex, 85 | LocationID: operation.LocationID, 86 | } 87 | 88 | } else if operation.Payload.Args.RequestSubRealm != "" { 89 | if !utils.IsValidSubRealm(operation.Payload.Args.RequestSubRealm) { 90 | return nil, nil, errors.ErrInvalidContainer 91 | } 92 | parentRealm, err := m.NftUTXOByAtomicalsID(operation.Payload.Args.ParentRealm) 93 | if err != nil { 94 | log.Log.Panicf("NftUTXOByAtomicalsID err:%v", err) 95 | } 96 | if parentRealm == nil { 97 | return nil, nil, errors.ErrParentRealmNotExist 98 | } 99 | isExist, err := m.NftSubRealmByNameHasExist(operation.Payload.Args.ParentRealm, operation.Payload.Args.RequestSubRealm) 100 | if err != nil { 101 | log.Log.Panicf("NftSubRealmByName err:%v", err) 102 | } 103 | if isExist { 104 | return nil, nil, errors.ErrSubRealmHasExist 105 | } 106 | if operation.Payload.IsImmutable() { 107 | return nil, nil, errors.ErrCannotBeImmutable 108 | } 109 | if operation.Payload.Args.ClaimType == witness.Rule { 110 | matched_rule := m.get_applicable_rule_by_height(operation.Payload.Args.ParentRealm, 111 | operation.Payload.Args.RequestSubRealm, operation.CommitHeight-utils.MINT_SUBNAME_RULES_BECOME_EFFECTIVE_IN_BLOCKS) 112 | if !m.checkRule(matched_rule, operation.Payload.Args.Bitworkc, operation.Payload.Args.Bitworkr) { 113 | return nil, nil, errors.ErrInvalidRule 114 | } 115 | } 116 | newUTXONftInfo = &postsql.UTXONftInfo{ 117 | UserPk: userPk, 118 | RealmName: parentRealm.RealmName, 119 | SubRealmName: operation.Payload.Args.RequestSubRealm, 120 | ClaimType: operation.Payload.Args.ClaimType, 121 | ParentRealmAtomicalsID: operation.Payload.Args.ParentRealm, 122 | Time: operation.Payload.Args.Time, 123 | Bitworkc: operation.Payload.Args.Bitworkc, 124 | Bitworkr: operation.Payload.Args.Bitworkr, 125 | AtomicalsID: operation.AtomicalsID, 126 | LocationID: operation.LocationID, 127 | } 128 | } else if operation.Payload.Args.RequestContainer != "" { 129 | if !utils.IsValidContainer(operation.Payload.Args.RequestContainer) { 130 | return nil, nil, errors.ErrInvalidContainer 131 | } 132 | isExist, err := m.NftContainerByNameHasExist(operation.Payload.Args.RequestContainer) 133 | if err != nil { 134 | log.Log.Panicf("NftContainerByName err:%v", err) 135 | } 136 | if isExist { 137 | return nil, nil, errors.ErrContainerHasExist 138 | } 139 | if operation.Payload.IsImmutable() { 140 | return nil, nil, errors.ErrCannotBeImmutable 141 | } 142 | if operation.Payload.Args.Bitworkc == "" { 143 | return nil, nil, errors.ErrBitworkcNeeded 144 | } 145 | newUTXONftInfo = &postsql.UTXONftInfo{ 146 | UserPk: userPk, 147 | ContainerName: operation.Payload.Args.RequestContainer, 148 | Time: operation.Payload.Args.Time, 149 | Bitworkc: operation.Payload.Args.Bitworkc, 150 | Bitworkr: operation.Payload.Args.Bitworkr, 151 | AtomicalsID: operation.AtomicalsID, 152 | LocationID: operation.LocationID, 153 | } 154 | bitworkc, _, err := operation.IsValidBitwork() 155 | if err != nil { 156 | return nil, nil, err 157 | } 158 | if bitworkc != nil && len(bitworkc.Prefix) < 4 { 159 | return nil, nil, errors.ErrInvalidBitworkcPrefix 160 | } 161 | } else if operation.Payload.Args.RequestDmitem != "" { 162 | if !utils.IsDmintActivated(operation.RevealLocationHeight) { 163 | return nil, nil, errors.ErrDmintNotStart 164 | } 165 | parentContainer, err := m.NftUTXOByAtomicalsID(operation.Payload.Args.ParentContainer) 166 | if err != nil { 167 | log.Log.Panicf("NftUTXOByAtomicalsID err:%v", err) 168 | } 169 | if parentContainer == nil { 170 | return nil, nil, errors.ErrContainerNotExist 171 | } 172 | 173 | if !utils.IsValidDmitem(operation.Payload.Args.RequestDmitem) { 174 | return nil, nil, errors.ErrInvalidContainerDmitem 175 | } 176 | isExist, err := m.ContainerItemByNameHasExist(parentContainer.ContainerName, operation.Payload.Args.RequestDmitem) 177 | if err != nil { 178 | log.Log.Panicf("ContainerItemByName err:%v", err) 179 | } 180 | if isExist { 181 | return nil, nil, errors.ErrSubRealmHasExist 182 | } 183 | if !m.verifyRuleAndMerkle(operation) { 184 | return nil, nil, errors.ErrInvalidMerkleVerify 185 | } 186 | newUTXONftInfo = &postsql.UTXONftInfo{ 187 | UserPk: userPk, 188 | ContainerName: parentContainer.ContainerName, 189 | Dmitem: operation.Payload.Args.RequestDmitem, 190 | ParentContainerAtomicalsID: operation.Payload.Args.ParentContainer, 191 | Time: operation.Payload.Args.Time, 192 | Bitworkc: operation.Payload.Args.Bitworkc, 193 | Bitworkr: operation.Payload.Args.Bitworkr, 194 | AtomicalsID: operation.AtomicalsID, 195 | LocationID: operation.LocationID, 196 | } 197 | } 198 | return newUTXONftInfo, deleteUTXONfts, nil 199 | } 200 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/referenceIndexer.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 11 | "github.com/atomicals-go/pkg/log" 12 | "github.com/atomicals-go/repo" 13 | ) 14 | 15 | func ReferenceIndexer(data *repo.AtomicaslData, operation *witness.WitnessAtomicalsOperation) { 16 | hasAtomicalsAsset, err := fetchAtomicalsAssetFromReferenceIndexer(operation.AtomicalsID) 17 | if err != nil { 18 | log.Log.Infof("RevealLocationTxID:%v", operation.RevealLocationTxID) 19 | panic(err) 20 | } 21 | 22 | switch operation.Op { 23 | case "dmt": 24 | case "dft": 25 | if hasAtomicalsAsset && data.NewGlobalDistributedFt == nil { 26 | log.Log.Infof("%v %v %v", operation.RevealLocationHeight, operation.AtomicalsID, operation.RevealLocationTxID) 27 | panic("hasAtomicalsAsset") 28 | } 29 | if !hasAtomicalsAsset && data.NewGlobalDistributedFt != nil { 30 | log.Log.Infof("%v %v %v", operation.RevealLocationHeight, operation.AtomicalsID, operation.RevealLocationTxID) 31 | panic("hasAtomicalsAsset") 32 | } 33 | case "ft": 34 | if hasAtomicalsAsset && data.NewGlobalDirectFt == nil { 35 | log.Log.Infof("%v %v %v", operation.RevealLocationHeight, operation.AtomicalsID, operation.RevealLocationTxID) 36 | panic("hasAtomicalsAsset") 37 | } 38 | if !hasAtomicalsAsset && data.NewGlobalDirectFt != nil { 39 | log.Log.Infof("%v %v %v", operation.RevealLocationHeight, operation.AtomicalsID, operation.RevealLocationTxID) 40 | panic("hasAtomicalsAsset") 41 | } 42 | case "nft": 43 | if hasAtomicalsAsset && data.NewUTXONftInfo == nil { 44 | if !(operation.Payload.Args.RequestContainer != "" || operation.Payload.Args.RequestDmitem != "") { 45 | return 46 | } 47 | log.Log.Infof("%v %v %v", operation.RevealLocationHeight, operation.AtomicalsID, operation.RevealLocationTxID) 48 | panic("hasAtomicalsAsset") 49 | } 50 | if !hasAtomicalsAsset && data.NewUTXONftInfo != nil { 51 | log.Log.Infof("%v %v %v", operation.RevealLocationHeight, operation.AtomicalsID, operation.RevealLocationTxID) 52 | panic("hasAtomicalsAsset") 53 | } 54 | case "evt": 55 | panic(operation.Payload) 56 | case "dat": 57 | case "sl": 58 | panic(operation.Payload) 59 | default: 60 | } 61 | 62 | } 63 | 64 | func fetchAtomicalsAssetFromReferenceIndexer(atomicalsID string) (bool, error) { 65 | encodedTxID := url.QueryEscape(fmt.Sprintf(`"%s"`, atomicalsID)) 66 | // endpoint := "https://ep.atomicals.xyz/proxy" 67 | // endpoint := "https://ep.atomicalswallet.com/proxy" 68 | endpoint := "https://ep.wizz.cash/proxy" 69 | // endpoint := "https://atomindexer.satsx.io/proxy" 70 | url := fmt.Sprintf(endpoint+"/blockchain.atomicals.get?params=[%s]", encodedTxID) 71 | resp, err := http.Get(url) 72 | if err != nil { 73 | return false, err 74 | } 75 | defer resp.Body.Close() 76 | 77 | body, err := ioutil.ReadAll(resp.Body) 78 | if err != nil { 79 | return false, err 80 | } 81 | // fmt.Println(string(body)) 82 | var response RespAtomicalsAssetFromReferenceIndexer 83 | err = json.Unmarshal(body, &response) 84 | if err != nil { 85 | return false, err 86 | } 87 | 88 | return response.Response.Result.Confirmed, nil 89 | } 90 | 91 | type RespAtomicalsAssetFromReferenceIndexer struct { 92 | Success bool `json:"success"` 93 | Response struct { 94 | Result struct { 95 | AtomicalID string `json:"atomical_id"` 96 | AtomicalNumber int `json:"atomical_number"` 97 | AtomicalRef string `json:"atomical_ref"` 98 | Type string `json:"type"` 99 | Confirmed bool `json:"confirmed"` 100 | MintInfo struct { 101 | CommitTxid string `json:"commit_txid"` 102 | CommitIndex int `json:"commit_index"` 103 | CommitLocation string `json:"commit_location"` 104 | CommitTxNum int `json:"commit_tx_num"` 105 | CommitHeight int `json:"commit_height"` 106 | RevealLocationTxid string `json:"reveal_location_txid"` 107 | RevealLocationIndex int `json:"reveal_location_index"` 108 | RevealLocation string `json:"reveal_location"` 109 | RevealLocationTxNum int `json:"reveal_location_tx_num"` 110 | RevealLocationHeight int `json:"reveal_location_height"` 111 | RevealLocationHeader string `json:"reveal_location_header"` 112 | RevealLocationBlockhash string `json:"reveal_location_blockhash"` 113 | RevealLocationScripthash string `json:"reveal_location_scripthash"` 114 | RevealLocationScript string `json:"reveal_location_script"` 115 | RevealLocationValue int `json:"reveal_location_value"` 116 | Args struct { 117 | MintAmount int `json:"mint_amount"` 118 | MintHeight int `json:"mint_height"` 119 | MaxMints int `json:"max_mints"` 120 | RequestTicker string `json:"request_ticker"` 121 | Bitworkc string `json:"bitworkc"` 122 | // Nonce string `json:"nonce"` 123 | Time int `json:"time"` 124 | } `json:"args"` 125 | Meta struct { 126 | } `json:"meta"` 127 | Ctx struct { 128 | } `json:"ctx"` 129 | RequestTicker string `json:"$request_ticker"` 130 | Bitwork struct { 131 | Bitworkc string `json:"bitworkc"` 132 | Bitworkr interface{} `json:"bitworkr"` 133 | } `json:"$bitwork"` 134 | } `json:"mint_info"` 135 | Subtype string `json:"subtype"` 136 | MintMode string `json:"$mint_mode"` 137 | MaxSupply int `json:"$max_supply"` 138 | MintHeight int `json:"$mint_height"` 139 | MintAmount int `json:"$mint_amount"` 140 | MaxMints int `json:"$max_mints"` 141 | Bitwork struct { 142 | Bitworkc string `json:"bitworkc"` 143 | Bitworkr interface{} `json:"bitworkr"` 144 | } `json:"$bitwork"` 145 | TickerCandidates []struct { 146 | TxNum int `json:"tx_num"` 147 | AtomicalID string `json:"atomical_id"` 148 | Txid string `json:"txid"` 149 | CommitHeight int `json:"commit_height"` 150 | RevealLocationHeight int `json:"reveal_location_height"` 151 | } `json:"$ticker_candidates"` 152 | RequestTickerStatus struct { 153 | Status string `json:"status"` 154 | VerifiedAtomicalID string `json:"verified_atomical_id"` 155 | Note string `json:"note"` 156 | } `json:"$request_ticker_status"` 157 | RequestTicker string `json:"$request_ticker"` 158 | Ticker string `json:"$ticker"` 159 | MintData struct { 160 | Fields struct { 161 | ImageJpg struct { 162 | Ct string `json:"$ct"` 163 | B struct { 164 | D string `json:"$d"` 165 | B string `json:"$b"` 166 | Len int `json:"$len"` 167 | Auto bool `json:"$auto"` 168 | } `json:"$b"` 169 | } `json:"image.jpg"` 170 | Args struct { 171 | MintAmount int `json:"mint_amount"` 172 | MintHeight int `json:"mint_height"` 173 | MaxMints int `json:"max_mints"` 174 | RequestTicker string `json:"request_ticker"` 175 | Bitworkc string `json:"bitworkc"` 176 | // Nonce string `json:"nonce"` 177 | Time int `json:"time"` 178 | } `json:"args"` 179 | } `json:"fields"` 180 | } `json:"mint_data"` 181 | DftInfo struct { 182 | MintCount int `json:"mint_count"` 183 | } `json:"dft_info"` 184 | LocationSummary struct { 185 | UniqueHolders int `json:"unique_holders"` 186 | CirculatingSupply int `json:"circulating_supply"` 187 | } `json:"location_summary"` 188 | } `json:"result"` 189 | } `json:"response"` 190 | } 191 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/rule.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 11 | "github.com/atomicals-go/utils" 12 | ) 13 | 14 | func (m *Atomicals) checkRule(rule *witness.RuleInfo, bitworkc_actual, bitworkr_actual string) bool { 15 | if rule == nil { 16 | return false 17 | } 18 | bitworkc := rule.Bitworkc 19 | bitworkr := rule.Bitworkr 20 | 21 | if bitworkc == "any" { 22 | return true 23 | } else { 24 | if bitworkc_actual != bitworkc { 25 | return false 26 | } 27 | } 28 | if bitworkr == "any" { 29 | return true 30 | } else { 31 | if bitworkr_actual != bitworkr { 32 | return false 33 | } 34 | } 35 | if rule.O != nil { 36 | return true 37 | } 38 | if bitworkc != "" || bitworkr != "" { 39 | return true 40 | } 41 | return false 42 | } 43 | func (m *Atomicals) get_applicable_rule_by_height(parent_atomical_id string, proposed_subnameid string, height int64) *witness.RuleInfo { 44 | latest_state, err := m.getModHistory(parent_atomical_id, height) 45 | if err != nil { 46 | panic(err) 47 | } 48 | regex_price_point_list := validateRulesData(latest_state.Rules) 49 | for _, regex_price_point := range regex_price_point_list { 50 | valid_pattern := regexp.MustCompile(regex_price_point.P) 51 | if !valid_pattern.MatchString(proposed_subnameid) { 52 | continue 53 | } 54 | return regex_price_point 55 | } 56 | return nil 57 | } 58 | 59 | // get_mod_history 60 | func (m *Atomicals) getModHistory(parentContainerAtomicalsID string, height int64) (*witness.Mod, error) { 61 | mods, err := m.ModHistory(parentContainerAtomicalsID, height) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if mods == nil { 66 | return nil, errors.New("invalid mod") 67 | } 68 | dmints := make([]*witness.Mod, 0) 69 | for _, mod := range mods { 70 | dmint := &witness.Mod{} 71 | if err := json.Unmarshal([]byte(mod.Mod), dmint); err != nil { 72 | return nil, err 73 | } 74 | dmints = append(dmints, dmint) 75 | } 76 | // calculate_latest_state_from_mod_history 77 | // Ensure it is sorted in ascending order 78 | // sort.Slice(mod_history, func(i, j int) bool { 79 | // return mod_history[i].ID < mod_history[j].ID 80 | // }) 81 | current_object_state := &witness.Mod{} 82 | for _, element := range dmints { 83 | if element.A == 1 { 84 | current_object_state = nil 85 | } else { 86 | current_object_state = element 87 | } 88 | } 89 | if current_object_state == nil { 90 | return nil, errors.New("invalid mod") 91 | } 92 | if validateRulesData(current_object_state.Rules) == nil { 93 | return nil, errors.New("invalid mod") 94 | } 95 | if current_object_state.MintHeight < 0 { 96 | return nil, errors.New("invalid mod") 97 | } 98 | if current_object_state.V != "1" { 99 | return nil, errors.New("invalid mod") 100 | } 101 | if len(current_object_state.Merkle) != 64 { 102 | return nil, errors.New("invalid mod") 103 | } 104 | return current_object_state, nil 105 | } 106 | 107 | func validateRulesData(rules []*witness.RuleInfo) []*witness.RuleInfo { 108 | if len(rules) <= 0 || len(rules) > utils.MAX_SUBNAME_RULE_ENTRIES { 109 | return nil 110 | } 111 | validated_rules_list := []*witness.RuleInfo{} 112 | for _, rule := range rules { 113 | regex_pattern := rule.P 114 | if len(regex_pattern) > utils.MAX_SUBNAME_RULE_SIZE_LEN || len(regex_pattern) < 1 { 115 | return nil 116 | } 117 | if strings.ContainsAny(regex_pattern, "()") { 118 | return nil 119 | } 120 | _, err := regexp.Compile(regex_pattern) 121 | if err != nil { 122 | fmt.Println("Regex compile error:", err) 123 | return nil 124 | } 125 | bitworkc := rule.Bitworkc 126 | bitworkr := rule.Bitworkr 127 | if regex_pattern == "" { 128 | return nil 129 | } 130 | if strings.Contains(regex_pattern, "(") || strings.Contains(regex_pattern, ")") { 131 | return nil 132 | } 133 | price_point := &witness.RuleInfo{ 134 | P: regex_pattern, 135 | } 136 | if bitworkc != "" { 137 | res := utils.ParseBitwork(bitworkc) 138 | if res != nil { 139 | price_point.Bitworkc = bitworkc 140 | } else if bitworkc == "any" { 141 | price_point.Bitworkc = bitworkc 142 | } else { 143 | return nil 144 | } 145 | } 146 | if bitworkr != "" { 147 | res := utils.ParseBitwork(bitworkr) 148 | if res != nil { 149 | price_point.Bitworkr = bitworkr 150 | } else if bitworkr == "any" { 151 | price_point.Bitworkr = bitworkr 152 | } else { 153 | return nil 154 | } 155 | } 156 | if len(rule.O) > 0 { 157 | if !validate_subrealm_rules_outputs_format(rule.O) { 158 | return nil 159 | } 160 | price_point.O = rule.O 161 | validated_rules_list = append(validated_rules_list, price_point) 162 | } else if bitworkc != "" || bitworkr != "" { 163 | validated_rules_list = append(validated_rules_list, price_point) 164 | } else { 165 | return nil 166 | } 167 | if rule.O == nil && bitworkc == "" && bitworkr == "" { 168 | return nil 169 | } 170 | } 171 | if validated_rules_list == nil { 172 | return nil 173 | } 174 | if len(validated_rules_list) == 0 { 175 | return nil 176 | } 177 | return validated_rules_list 178 | } 179 | 180 | func validate_subrealm_rules_outputs_format(outputs map[string]*witness.Output) bool { 181 | for expected_output_script, expected_output_value := range outputs { 182 | expected_output_id := expected_output_value.ID 183 | expected_output_qty := expected_output_value.V 184 | if expected_output_qty < utils.SUBNAME_MIN_PAYMENT_DUST_LIMIT { 185 | return false // # Reject if one of the entries expects less than the minimum payment amount 186 | } 187 | // # If there is a type restriction on the payment type then ensure it is a valid atomical id 188 | if expected_output_id != "" { 189 | if utils.IsCompactAtomicalID(expected_output_id) { 190 | return false 191 | } 192 | } 193 | // # script must be paid to mint a subrealm 194 | if !utils.IsHexString(expected_output_script) { 195 | return false // # Reject if one of the payment output script is not a valid hex 196 | } 197 | } 198 | return true 199 | } 200 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/syncTxHeight.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 5 | ) 6 | 7 | func (m *Atomicals) SyncTxHeight() { 8 | height := m.location.BlockHeight + 2 9 | for { 10 | m.syncTxHeight(height) 11 | height++ 12 | } 13 | } 14 | func (m *Atomicals) syncTxHeight(height int64) { 15 | block, err := m.GetBlockByHeight(height) 16 | if err != nil { 17 | return 18 | } 19 | for _, tx := range block.Tx { 20 | operation := witness.ParseWitness(tx, block.Height) 21 | if operation.Op == "" { 22 | continue 23 | } 24 | commitHeight, err := m.GetTxHeightByTxID(operation.CommitTxID) 25 | if err != nil { 26 | panic(err) 27 | } 28 | if _, ok := m.SyncTxHeightMap.Load(operation.CommitTxID); !ok { 29 | m.SyncTxHeightMap.Store(operation.CommitTxID, commitHeight) 30 | } 31 | } 32 | } 33 | 34 | func (m *Atomicals) getTxHeight(commitTxID string) int64 { 35 | h, err := m.GetTxHeightByTxID(commitTxID) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return h 40 | // height, ok := m.SyncTxHeightMap.LoadAndDelete(commitTxID) 41 | // if ok { 42 | // h, _ := height.(int64) 43 | // return h 44 | // } else { 45 | // var err error 46 | // h, err := m.GetTxHeightByTxID(commitTxID) 47 | // if err != nil { 48 | // panic(err) 49 | // } 50 | // m.SyncTxHeightMap.Delete(commitTxID) 51 | // return h 52 | // } 53 | } 54 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/traceTx.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 7 | "github.com/atomicals-go/pkg/log" 8 | "github.com/atomicals-go/repo" 9 | "github.com/atomicals-go/utils" 10 | "github.com/btcsuite/btcd/btcjson" 11 | ) 12 | 13 | func (m *Atomicals) Run() { 14 | var startTime1 time.Duration 15 | var startTime2 time.Duration 16 | 17 | startTime := time.Now() 18 | if m.location.BlockHeight+utils.SafeBlockHeightInterupt > m.maxBlockHeight { 19 | time.Sleep(10 * time.Minute) 20 | var err error 21 | m.maxBlockHeight, err = m.GetBlockCount() 22 | if err != nil { 23 | log.Log.Panicf("GetBlockCount err:%v", err) 24 | } 25 | return 26 | } 27 | block, err := m.GetBlockByHeightSync(m.location.BlockHeight) 28 | if err != nil { 29 | log.Log.Panicf("GetBlockByHeightSync err:%v", err) 30 | } 31 | m.location.TxIndex++ 32 | if m.location.TxIndex >= int64(len(block.Tx)) { 33 | m.location.BlockHeight++ 34 | m.location.TxIndex = 0 35 | block, err = m.GetBlockByHeightSync(m.location.BlockHeight) 36 | if err != nil { 37 | log.Log.Panicf("GetBlockByHeightSync err:%v", err) 38 | } 39 | } 40 | for txIndex, tx := range block.Tx { 41 | if int64(txIndex) < m.location.TxIndex { 42 | continue 43 | } 44 | startTime := time.Now() 45 | m.location.TxIndex = int64(txIndex) 46 | m.location.Txid = tx.Txid 47 | data := m.TraceTx(tx, block.Height) 48 | startTime1 = startTime1 + time.Since(startTime) 49 | 50 | startTime = time.Now() 51 | err = m.UpdateDB(m.location, data) 52 | if err != nil { 53 | log.Log.Panicf("UpdateDB err:%v", err) 54 | } 55 | startTime2 = startTime2 + time.Since(startTime) 56 | // log.Log.Infof("maxBlockHeight:%v, currentHeight:%v, %v lenTx:%v time:%v %v %v", m.maxBlockHeight, m.location.BlockHeight, len(block.Tx), txIndex, time.Since(startTime), startTime1, startTime2) 57 | } 58 | log.Log.Infof("maxBlockHeight:%v, currentHeight:%v,lenTx:%v time:%v %v %v", m.maxBlockHeight, m.location.BlockHeight, len(block.Tx), time.Since(startTime), startTime1, startTime2) 59 | } 60 | 61 | func (m *Atomicals) TraceTx(tx btcjson.TxRawResult, height int64) *repo.AtomicaslData { 62 | operation := witness.ParseWitness(tx, height) 63 | 64 | data := &repo.AtomicaslData{} 65 | 66 | // step 1: insert mod 67 | if operation.Op == "mod" { 68 | data.Mod = m.operationMod(operation, tx) 69 | } 70 | 71 | // step 2: transfer nft first, then transfer ft 72 | data.UpdateNfts, _ = m.transferNft(operation, tx) 73 | 74 | data.DeleteFts, data.NewFts, _ = m.transferFt(operation, tx) 75 | 76 | // step 3: process operation 77 | userPk := tx.Vout[utils.VOUT_EXPECT_OUTPUT_INDEX].ScriptPubKey.Address 78 | if operation.Op == "dmt" { 79 | data.NewUTXOFtInfo, data.UpdateDistributedFt, _ = m.mintDistributedFt(operation, tx.Vout, userPk) 80 | } else { 81 | switch operation.Op { 82 | case "dft": 83 | data.NewGlobalDistributedFt, _ = m.deployDistributedFt(operation, userPk) 84 | case "ft": 85 | data.NewGlobalDirectFt, _ = m.mintDirectFt(operation, tx.Vout, userPk) 86 | case "nft": 87 | if operation.Payload.Args.RequestContainer != "" || operation.Payload.Args.RequestDmitem != "" { 88 | data.NewUTXONftInfo, data.DeleteUTXONfts, _ = m.mintNft(operation, userPk) 89 | } 90 | case "evt": 91 | panic(operation.Payload) 92 | case "dat": 93 | data.Dat = m.operationDat(operation, tx) 94 | case "sl": 95 | panic(operation.Payload) 96 | default: 97 | } 98 | } 99 | 100 | // TODO: step 4 check payment 101 | 102 | data.ParseOperation(operation.Op) 103 | return data 104 | } 105 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/transferFt.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "encoding/hex" 5 | "sort" 6 | 7 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 8 | "github.com/atomicals-go/pkg/log" 9 | "github.com/atomicals-go/repo/postsql" 10 | "github.com/atomicals-go/utils" 11 | "github.com/btcsuite/btcd/btcjson" 12 | ) 13 | 14 | func (m *Atomicals) transferFt(operation *witness.WitnessAtomicalsOperation, tx btcjson.TxRawResult) (deleteFts []*postsql.UTXOFtInfo, newFts []*postsql.UTXOFtInfo, err error) { 15 | if operation.IsSplitOperation() { // color_ft_atomicals_split 16 | for _, vin := range tx.Vin { 17 | preLocationID := utils.AtomicalsID(vin.Txid, int64(vin.Vout)) 18 | preFts, err := m.FtUTXOsByLocationID(preLocationID) 19 | if err != nil { 20 | log.Log.Panicf("FtUTXOsByLocationID err:%v", err) 21 | } 22 | if len(preFts) == 0 { 23 | continue 24 | } 25 | deleteFts = append(deleteFts, preFts...) 26 | } 27 | sort.Slice(deleteFts, func(i, j int) bool { 28 | return deleteFts[i].AtomicalsID < deleteFts[j].AtomicalsID 29 | }) 30 | for _, ft := range deleteFts { 31 | totalAmountToSkipPotential := operation.Payload.Args.TotalAmountToSkipPotential[ft.LocationID] 32 | remainingValue := ft.Amount 33 | for outputIndex, vout := range tx.Vout { 34 | amount := utils.MulSatoshi(vout.Value) 35 | if 0 < totalAmountToSkipPotential { 36 | totalAmountToSkipPotential -= amount 37 | continue 38 | } 39 | if remainingValue < amount { // burn rest ft 40 | break 41 | } 42 | remainingValue -= amount 43 | locationID := utils.AtomicalsID(operation.RevealLocationTxID, int64(outputIndex)) 44 | newFts = append(newFts, &postsql.UTXOFtInfo{ 45 | UserPk: vout.ScriptPubKey.Address, 46 | AtomicalsID: ft.AtomicalsID, 47 | LocationID: locationID, 48 | MintTicker: ft.MintTicker, 49 | Time: ft.Time, 50 | Bitworkc: ft.Bitworkc, 51 | Bitworkr: ft.Bitworkr, 52 | MintBitworkVec: ft.MintBitworkVec, 53 | MintBitworkcInc: ft.MintBitworkcInc, 54 | MintBitworkrInc: ft.MintBitworkrInc, 55 | Amount: amount, 56 | }) 57 | } 58 | } 59 | } else { // color_ft_atomicals_regular 60 | // a ft in Vin has a total amount: entity.Amount, 61 | // color exactly the same amount of vout 62 | // burn the rest ft 63 | for _, vin := range tx.Vin { 64 | preLocationID := utils.AtomicalsID(vin.Txid, int64(vin.Vout)) 65 | preFts, err := m.FtUTXOsByLocationID(preLocationID) 66 | if err != nil { 67 | log.Log.Panicf("FtUTXOsByLocationID err:%v", err) 68 | } 69 | if len(preFts) == 0 { 70 | continue 71 | } 72 | deleteFts = append(deleteFts, preFts...) 73 | } 74 | sort.Slice(deleteFts, func(i, j int) bool { 75 | return deleteFts[i].AtomicalsID < deleteFts[j].AtomicalsID 76 | }) 77 | deleteFtMap := make(map[string][]*postsql.UTXOFtInfo, 0) 78 | for _, ft := range deleteFts { 79 | if _, ok := deleteFtMap[ft.AtomicalsID]; !ok { 80 | deleteFtMap[ft.AtomicalsID] = make([]*postsql.UTXOFtInfo, 0) 81 | } 82 | deleteFtMap[ft.AtomicalsID] = append(deleteFtMap[ft.AtomicalsID], ft) 83 | } 84 | // calculate_outputs_to_color_for_ft_atomical_ids 85 | newFts = m.AssignFt(tx, operation, deleteFtMap) 86 | } 87 | return deleteFts, newFts, nil 88 | } 89 | 90 | func (m *Atomicals) AssignFt(tx btcjson.TxRawResult, operation *witness.WitnessAtomicalsOperation, deleteFtMap map[string][]*postsql.UTXOFtInfo) []*postsql.UTXOFtInfo { 91 | newFts := make([]*postsql.UTXOFtInfo, 0) 92 | for _, ftSlice := range deleteFtMap { 93 | voutRemainingSpace := make([]int64, len(tx.Vout)) 94 | for i, vout := range tx.Vout { 95 | voutRemainingSpace[i] = utils.MulSatoshi(vout.Value) 96 | } 97 | newFtAmount := int64(0) 98 | outputIndex := int64(0) 99 | for i, ft := range ftSlice { 100 | ftAmount := ft.Amount 101 | for { 102 | if outputIndex >= int64(len(tx.Vout)) { 103 | break 104 | } 105 | vout := tx.Vout[outputIndex] 106 | scriptPubKeyBytes, err := hex.DecodeString(vout.ScriptPubKey.Hex) 107 | if err != nil { 108 | panic(err) 109 | } 110 | if utils.IsUnspendableGenesis(scriptPubKeyBytes) || 111 | utils.IsUnspendableLegacy(scriptPubKeyBytes) { 112 | outputIndex = outputIndex + 1 113 | continue 114 | } 115 | locationID := utils.AtomicalsID(operation.RevealLocationTxID, int64(outputIndex)) 116 | if voutRemainingSpace[outputIndex] > ftAmount { // burn rest ft 117 | voutRemainingSpace[outputIndex] = voutRemainingSpace[outputIndex] - ftAmount 118 | newFtAmount += ftAmount 119 | if utils.IsCustomColoring(operation.RevealLocationHeight) && i == (len(ftSlice)-1) { 120 | newFts = append(newFts, &postsql.UTXOFtInfo{ 121 | UserPk: vout.ScriptPubKey.Address, 122 | AtomicalsID: ft.AtomicalsID, 123 | LocationID: locationID, 124 | MintTicker: ft.MintTicker, 125 | Time: ft.Time, 126 | Bitworkc: ft.Bitworkc, 127 | Bitworkr: ft.Bitworkr, 128 | MintBitworkVec: ft.MintBitworkVec, 129 | MintBitworkcInc: ft.MintBitworkcInc, 130 | MintBitworkrInc: ft.MintBitworkrInc, 131 | Amount: newFtAmount, 132 | }) 133 | } 134 | break 135 | } else if voutRemainingSpace[outputIndex] == ftAmount { // burn rest ft 136 | voutRemainingSpace[outputIndex] = voutRemainingSpace[outputIndex] - ftAmount 137 | newFtAmount += ftAmount 138 | outputIndex = outputIndex + 1 139 | newFts = append(newFts, &postsql.UTXOFtInfo{ 140 | UserPk: vout.ScriptPubKey.Address, 141 | AtomicalsID: ft.AtomicalsID, 142 | LocationID: locationID, 143 | MintTicker: ft.MintTicker, 144 | Time: ft.Time, 145 | Bitworkc: ft.Bitworkc, 146 | Bitworkr: ft.Bitworkr, 147 | MintBitworkVec: ft.MintBitworkVec, 148 | MintBitworkcInc: ft.MintBitworkcInc, 149 | MintBitworkrInc: ft.MintBitworkrInc, 150 | Amount: newFtAmount, 151 | }) 152 | newFtAmount = 0 153 | break 154 | } else if voutRemainingSpace[outputIndex] < ftAmount { 155 | voutRemainingSpace[outputIndex] = 0 156 | newFts = append(newFts, &postsql.UTXOFtInfo{ 157 | UserPk: vout.ScriptPubKey.Address, 158 | AtomicalsID: ft.AtomicalsID, 159 | LocationID: locationID, 160 | MintTicker: ft.MintTicker, 161 | Time: ft.Time, 162 | Bitworkc: ft.Bitworkc, 163 | Bitworkr: ft.Bitworkr, 164 | MintBitworkVec: ft.MintBitworkVec, 165 | MintBitworkcInc: ft.MintBitworkcInc, 166 | MintBitworkrInc: ft.MintBitworkrInc, 167 | Amount: utils.MulSatoshi(vout.Value), 168 | }) 169 | if outputIndex != int64(len(tx.Vout))-1 { 170 | ftAmount -= utils.MulSatoshi(vout.Value) 171 | newFtAmount = 0 172 | } 173 | outputIndex = outputIndex + 1 174 | } 175 | } 176 | } 177 | } 178 | return newFts 179 | } 180 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/operation/transferNft.go: -------------------------------------------------------------------------------- 1 | package atomicals 2 | 3 | import ( 4 | "encoding/hex" 5 | "sort" 6 | 7 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 8 | "github.com/atomicals-go/pkg/log" 9 | "github.com/atomicals-go/repo/postsql" 10 | "github.com/atomicals-go/utils" 11 | "github.com/btcsuite/btcd/btcjson" 12 | ) 13 | 14 | func (m *Atomicals) transferNft(operation *witness.WitnessAtomicalsOperation, tx btcjson.TxRawResult) (updateNfts []*postsql.UTXONftInfo, err error) { 15 | if operation.IsSplatOperation() { // calculate_nft_atomicals_splat 16 | atomicalsNfts := make([]*postsql.UTXONftInfo, 0) 17 | for _, vin := range tx.Vin { 18 | preNftLocationID := utils.AtomicalsID(vin.Txid, int64(vin.Vout)) 19 | preNfts, err := m.NftUTXOsByLocationID(preNftLocationID) 20 | if err != nil { 21 | log.Log.Panicf("NftUTXOsByLocationID err:%v", err) 22 | } 23 | if preNfts == nil { 24 | continue 25 | } 26 | if len(preNfts) == 0 { 27 | continue 28 | } 29 | atomicalsNfts = append(atomicalsNfts, preNfts...) 30 | } 31 | sort.Slice(atomicalsNfts, func(i, j int) bool { 32 | return atomicalsNfts[i].AtomicalsID < atomicalsNfts[j].AtomicalsID 33 | }) 34 | expectedOutputIndexIncrementing := int64(0) // # Begin assigning splatted atomicals at the 0'th index 35 | for _, nft := range atomicalsNfts { 36 | outputIndex := expectedOutputIndexIncrementing 37 | scriptPubKeyBytes, err := hex.DecodeString(tx.Vout[outputIndex].ScriptPubKey.Hex) 38 | if err != nil { 39 | panic("err") 40 | } 41 | if outputIndex >= int64(len(tx.Vout)) || 42 | utils.IsUnspendableGenesis(scriptPubKeyBytes) || 43 | utils.IsUnspendableLegacy(scriptPubKeyBytes) { 44 | outputIndex = int64(0) 45 | } 46 | nft.LocationID = utils.AtomicalsID(tx.Txid, outputIndex) 47 | nft.UserPk = tx.Vout[outputIndex].ScriptPubKey.Address 48 | updateNfts = append(updateNfts, nft) 49 | expectedOutputIndexIncrementing += 1 50 | } 51 | } else { // build_nft_input_idx_to_atomical_map && calculate_nft_atomicals_regular 52 | if utils.IsDmintActivated(operation.RevealLocationHeight) { 53 | inputIdxToAtomicalIdsMap := make(map[int64][]*postsql.UTXONftInfo, 0) // key txInIndex 54 | for vinIndex, vin := range tx.Vin { 55 | preNftLocationID := utils.AtomicalsID(vin.Txid, int64(vin.Vout)) 56 | preNfts, err := m.NftUTXOsByLocationID(preNftLocationID) 57 | if err != nil { 58 | log.Log.Panicf("NftUTXOsByLocationID err:%v", err) 59 | } 60 | if preNfts == nil { 61 | continue 62 | } 63 | if len(preNfts) == 0 { 64 | continue 65 | } 66 | inputIdxToAtomicalIdsMap[int64(vinIndex)] = preNfts 67 | } 68 | nextOutputIdx := int64(0) 69 | foundAtomicalAtInput := false 70 | for _, nfts := range inputIdxToAtomicalIdsMap { 71 | foundAtomicalAtInput = true 72 | expectedOutputIndex := nextOutputIdx 73 | if expectedOutputIndex >= int64(len(tx.Vout)) { 74 | expectedOutputIndex = int64(0) 75 | } 76 | scriptPubKeyBytes, err := hex.DecodeString(tx.Vout[expectedOutputIndex].ScriptPubKey.Hex) 77 | if err != nil { 78 | panic(err) 79 | } 80 | if utils.IsUnspendableGenesis(scriptPubKeyBytes) || 81 | utils.IsUnspendableLegacy(scriptPubKeyBytes) || 82 | operation.IsSplitOperation() { 83 | expectedOutputIndex = int64(0) 84 | } 85 | for _, nft := range nfts { 86 | nft.LocationID = utils.AtomicalsID(tx.Txid, expectedOutputIndex) 87 | nft.UserPk = tx.Vout[expectedOutputIndex].ScriptPubKey.Address 88 | updateNfts = append(updateNfts, nft) 89 | } 90 | if foundAtomicalAtInput { 91 | nextOutputIdx++ 92 | } 93 | } 94 | } else { // calculate_nft_output_index_legacy 95 | for vinIndex, vin := range tx.Vin { 96 | preNftLocationID := utils.AtomicalsID(vin.Txid, int64(vin.Vout)) 97 | preNfts, err := m.NftUTXOsByLocationID(preNftLocationID) 98 | if err != nil { 99 | log.Log.Panicf("NftUTXOsByLocationID err:%v", err) 100 | } 101 | if preNfts == nil { 102 | continue 103 | } 104 | if len(preNfts) == 0 { 105 | continue 106 | } 107 | expectedOutputIndex := int64(vinIndex) 108 | // # Assign NFTs the legacy way with 1:1 inputs to outputs 109 | // # If it was unspendable output, then just set it to the 0th location 110 | // # ...and never allow an NFT atomical to be burned accidentally by having insufficient number of outputs either 111 | // # The expected output index will become the 0'th index if the 'x' extract operation was specified or there are insufficient outputs 112 | // # If this was the 'split' (y) command, then also move them to the 0th output 113 | if expectedOutputIndex >= int64(len(tx.Vout)) { 114 | expectedOutputIndex = int64(0) 115 | } 116 | scriptPubKeyBytes, err := hex.DecodeString(tx.Vout[expectedOutputIndex].ScriptPubKey.Hex) 117 | if err != nil { 118 | panic(err) 119 | } 120 | if utils.IsUnspendableGenesis(scriptPubKeyBytes) || 121 | utils.IsUnspendableLegacy(scriptPubKeyBytes) || 122 | operation.IsSplitOperation() { 123 | expectedOutputIndex = int64(0) 124 | } 125 | for _, nft := range preNfts { 126 | nft.LocationID = utils.AtomicalsID(tx.Txid, expectedOutputIndex) 127 | nft.UserPk = tx.Vout[expectedOutputIndex].ScriptPubKey.Address 128 | updateNfts = append(updateNfts, nft) 129 | } 130 | } 131 | } 132 | } 133 | return updateNfts, nil 134 | } 135 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/check.go: -------------------------------------------------------------------------------- 1 | package witness 2 | 3 | import ( 4 | "github.com/atomicals-go/pkg/errors" 5 | "github.com/atomicals-go/utils" 6 | ) 7 | 8 | // is_dft_bitwork_rollover_activated 9 | func (m *WitnessAtomicalsOperation) IsDftBitworkRolloverActivated() bool { 10 | return m.RevealLocationHeight >= utils.ATOMICALS_ACTIVATION_HEIGHT_DFT_BITWORK_ROLLOVER 11 | } 12 | 13 | // is_within_acceptable_blocks_for_name_reveal 14 | func (m *WitnessAtomicalsOperation) IsWithinAcceptableBlocksForNameReveal() bool { 15 | return m.CommitHeight >= m.RevealLocationHeight-utils.MINT_REALM_CONTAINER_TICKER_COMMIT_REVEAL_DELAY_BLOCKS 16 | } 17 | 18 | // is_within_acceptable_blocks_for_general_reveal 19 | func (m *WitnessAtomicalsOperation) IsWithinAcceptableBlocksForGeneralReveal() bool { 20 | return m.CommitHeight >= m.RevealLocationHeight-utils.MINT_GENERAL_COMMIT_REVEAL_DELAY_BLOCKS 21 | } 22 | 23 | func (m *WitnessAtomicalsOperation) IsValidBitwork() (*utils.Bitwork, *utils.Bitwork, error) { 24 | if m.Payload == nil { 25 | return nil, nil, nil 26 | } 27 | if m.Payload.Args == nil { 28 | return nil, nil, nil 29 | } 30 | bitworkc := utils.ParseBitwork(m.Payload.Args.Bitworkc) 31 | if bitworkc != nil { 32 | if !utils.IsProofOfWorkPrefixMatch(m.CommitTxID, bitworkc.Prefix, bitworkc.Ext) { 33 | return nil, nil, errors.ErrInvalidBitWork 34 | } 35 | } 36 | bitworkr := utils.ParseBitwork(m.Payload.Args.Bitworkr) 37 | if bitworkr != nil { 38 | if !utils.IsProofOfWorkPrefixMatch(m.CommitTxID, bitworkr.Prefix, bitworkr.Ext) { 39 | return nil, nil, errors.ErrInvalidBitWork 40 | } 41 | } 42 | return bitworkc, bitworkr, nil 43 | } 44 | 45 | // is_splat_operation 46 | func (m *WitnessAtomicalsOperation) IsSplatOperation() bool { 47 | return m != nil && m.Op == "x" && m.RevealInputIndex == 0 48 | } 49 | 50 | // is_split_operation 51 | func (m *WitnessAtomicalsOperation) IsSplitOperation() bool { 52 | return m != nil && m.Op == "y" && m.RevealInputIndex == 0 53 | } 54 | 55 | // is_seal_operation 56 | func (m *WitnessAtomicalsOperation) Is_seal_operation() bool { 57 | return m != nil && m.Op == "sl" && m.RevealInputIndex == 0 58 | } 59 | 60 | // is_event_operation 61 | func (m *WitnessAtomicalsOperation) Is_event_operation() bool { 62 | return m != nil && m.Op == "evt" && m.RevealInputIndex == 0 63 | } 64 | 65 | // is_immutable 66 | func (m *PayLoad) IsImmutable() bool { 67 | if m == nil { 68 | return false 69 | } 70 | if m.Args == nil { 71 | return false 72 | } 73 | return m.Args.I 74 | } 75 | 76 | func (m *PayLoad) CheckRequest() bool { 77 | if m.Args == nil { 78 | return false 79 | } else { 80 | request_counter := 0 // # Ensure that only one of the following may be requested || fail 81 | if m.Args.RequestRealm != "" { 82 | request_counter += 1 83 | } 84 | if m.Args.RequestSubRealm != "" { 85 | request_counter += 1 86 | } 87 | if m.Args.RequestContainer != "" { 88 | request_counter += 1 89 | } 90 | if m.Args.RequestTicker != "" { 91 | request_counter += 1 92 | } 93 | if m.Args.RequestDmitem != "" { 94 | request_counter += 1 95 | } 96 | if request_counter > 1 { 97 | return false 98 | } 99 | } 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/operation.go: -------------------------------------------------------------------------------- 1 | package witness 2 | 3 | import "encoding/hex" 4 | 5 | // parse_operation_from_script 6 | func parseAtomicalsOperation(scriptBytes []byte, startIndex int64) (string, int64) { 7 | one_letter_op_len := int64(2) 8 | two_letter_op_len := int64(3) 9 | three_letter_op_len := int64(4) 10 | operationType := "" 11 | 12 | // # check the 3 letter protocol operations 13 | atomOp := scriptBytes[startIndex : startIndex+three_letter_op_len] 14 | switch hex.EncodeToString(atomOp) { 15 | case "036e6674": 16 | operationType = "nft" // nft - Mint non-fungible token 17 | case "03646674": 18 | operationType = "dft" // dft - Deploy distributed mint fungible token starting point 19 | case "036d6f64": 20 | operationType = "mod" // mod - Modify general state 21 | case "03657674": 22 | operationType = "evt" // evt - Message response/reply 23 | case "03646d74": 24 | operationType = "dmt" // dmt - Mint tokens of distributed mint type (dft) 25 | case "03646174": 26 | operationType = "dat" // dat - Store data on a transaction (dat) 27 | } 28 | if operationType != "" { 29 | return operationType, startIndex + three_letter_op_len 30 | } 31 | // # check the 2 letter protocol operations 32 | atomOp = scriptBytes[startIndex : startIndex+two_letter_op_len] 33 | switch hex.EncodeToString(atomOp) { 34 | case "026674": 35 | operationType = "ft" //# ft - Mint fungible token with direct fixed supply 36 | case "02736c": 37 | operationType = "sl" //# sl - Seal an NFT and lock it from further changes forever 38 | } 39 | if operationType != "" { 40 | return operationType, startIndex + two_letter_op_len 41 | } 42 | // # check the 1 letter protocol operations 43 | atomOp = scriptBytes[startIndex : startIndex+one_letter_op_len] 44 | switch hex.EncodeToString(atomOp) { 45 | case "0178": 46 | operationType = "x" //# extract - move atomical to 0'th output 47 | case "0179": 48 | operationType = "y" //# split - 49 | case "017a": 50 | operationType = "z" //# partical colored 51 | 52 | } 53 | if operationType != "" { 54 | return operationType, startIndex + one_letter_op_len 55 | } 56 | return operationType, -1 57 | } 58 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/payload.go: -------------------------------------------------------------------------------- 1 | package witness 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/atomicals-go/pkg/errors" 7 | "github.com/atomicals-go/utils" 8 | "github.com/fxamacker/cbor/v2" 9 | ) 10 | 11 | type PayLoad struct { 12 | Args *Args `cbor:"args"` 13 | A int `cbor:"$a"` // for mod 14 | Dmint *Mod `cbor:"dmint"` // for mod 15 | Subrealms *Mod `cbor:"subrealms"` // for mod 16 | Meta *Meta `cbor:"meta"` 17 | } 18 | 19 | type Args struct { 20 | // Nonce int64 `cbor:"nonce"` 21 | Time int64 `cbor:"time"` 22 | 23 | // optional 24 | Bitworkc string `cbor:"bitworkc"` 25 | Bitworkr string `cbor:"bitworkr"` 26 | 27 | Immutable bool `cbor:"$immutable"` 28 | I bool `cbor:"i"` 29 | Main string `cbor:"main"` 30 | DynamicFields map[string][]byte `cbor:"-"` // use for Main, sometimes, Main is "image.png" or unsure name... so bad a rule 31 | Proof []Proof `cbor:"proof"` 32 | Parents map[string]int64 `cbor:"$parents"` // key: parent_atomical_id, value: , haven't catch this param, used in operation:nft 33 | 34 | // for dft & ft 35 | RequestTicker string `cbor:"request_ticker"` 36 | 37 | // for dft 38 | MintAmount int64 `cbor:"mint_amount"` 39 | MintHeight int64 `cbor:"mint_height"` 40 | MaxMints int64 `cbor:"max_mints"` 41 | MintBitworkc string `cbor:"mint_bitworkc"` 42 | MintBitworkr string `cbor:"mint_bitworkr"` 43 | Md string `cbor:"md"` // emu:"", "0", "1" 44 | Bv string `cbor:"bv"` 45 | Bci string `cbor:"bci"` 46 | Bri string `cbor:"bri"` 47 | Bcs int64 `cbor:"bcs"` 48 | Brs int64 `cbor:"brs"` 49 | Maxg int64 `cbor:"maxg"` 50 | 51 | // for dmt 52 | MintTicker string `cbor:"mint_ticker"` // mint ft name 53 | 54 | // for nft: realm 55 | RequestRealm string `cbor:"request_realm"` 56 | 57 | // for nft: subrealm 58 | RequestSubRealm string `cbor:"request_subrealm"` 59 | ClaimType NftSubrealmClaimType `cbor:"claim_type"` // enum: "direct" "rule" 60 | ParentRealm string `cbor:"parent_realm"` // ParentRealm atomicalsID 61 | 62 | // for nft: container 63 | RequestContainer string `cbor:"request_container"` 64 | 65 | // for nft: dmitem 66 | RequestDmitem string `cbor:"request_dmitem"` // item num in ParentContainer 67 | ParentContainer string `cbor:"parent_container"` // ParentContainer atomicalsID 68 | 69 | // for y 70 | TotalAmountToSkipPotential map[string]int64 // key: locationID 71 | } 72 | 73 | type NftSubrealmClaimType string 74 | 75 | const ( 76 | Direct NftSubrealmClaimType = "direct" 77 | Rule NftSubrealmClaimType = "rule" 78 | ) 79 | 80 | type Meta struct { 81 | Name string `cbor:"name"` 82 | Description string `cbor:"description"` 83 | Legal struct { 84 | Terms string `cbor:"terms"` 85 | } `cbor:"legal"` 86 | } 87 | 88 | type Proof struct { 89 | D string `cbor:"d"` 90 | P bool `cbor:"p"` 91 | } 92 | 93 | type Mod struct { 94 | A int64 `cbor:"$a"` 95 | V string `cbor:"v"` 96 | Items int64 `cbor:"items"` 97 | Rules []*RuleInfo `cbor:"rules"` 98 | Merkle string `cbor:"merkle"` 99 | Immutable bool `cbor:"immutable"` 100 | MintHeight int64 `cbor:"mint_height"` 101 | } 102 | 103 | type RuleInfo struct { 104 | P string `cbor:"p"` 105 | O map[string]*Output `cbor:"o"` // key: 106 | Bitworkc string `cbor:"bitworkc"` 107 | Bitworkr string `cbor:"bitworkr"` 108 | } 109 | type Output struct { 110 | ID string `cbor:"id"` 111 | V int64 `cbor:"v"` 112 | } 113 | 114 | type Subrealms struct { 115 | Rules []*RuleInfo `cbor:"rules"` 116 | } 117 | 118 | func parseOperationAndPayLoad(script string, height int64) (string, *PayLoad, error) { 119 | scriptBytes, err := hex.DecodeString(script) 120 | if err != nil { 121 | return "", nil, err 122 | } 123 | scriptEntryLen := int64(len(scriptBytes)) 124 | if scriptEntryLen < 39 || scriptBytes[0] != 0x20 { 125 | return "", nil, errors.ErrInvalidWitnessScriptLength 126 | } 127 | for index := int64(35); index < scriptEntryLen-6; index++ { 128 | opFlag := scriptBytes[index] 129 | if opFlag != OP_IF { 130 | continue 131 | } 132 | if hex.EncodeToString(scriptBytes[index+1:index+6]) != utils.ATOMICALS_ENVELOPE_MARKER_BYTES { 133 | continue 134 | } 135 | operation, startIndex := parseAtomicalsOperation(scriptBytes, index+6) 136 | if operation == "" { 137 | continue 138 | } 139 | payloadBytes, err := parseAtomicalsData(scriptBytes, startIndex) 140 | if err != nil { 141 | return "", nil, err 142 | } 143 | if payloadBytes == nil { 144 | continue 145 | } 146 | // get DynamicFields[main] 147 | payload := &PayLoad{ 148 | Args: &Args{ 149 | DynamicFields: map[string][]byte{}, 150 | TotalAmountToSkipPotential: make(map[string]int64, 0), 151 | }, 152 | } 153 | if err := cbor.Unmarshal(payloadBytes, payload); err != nil { 154 | return "", nil, err 155 | } 156 | tempMap := map[string]interface{}{} 157 | if err := cbor.Unmarshal(payloadBytes, &tempMap); err != nil { 158 | return "", nil, err 159 | } 160 | if _, ok := tempMap[payload.Args.Main]; ok { 161 | _, ok = tempMap[payload.Args.Main].([]byte) 162 | if ok { 163 | payload.Args.DynamicFields[payload.Args.Main] = tempMap[payload.Args.Main].([]byte) 164 | } 165 | } 166 | // get TotalAmountToSkipPotential 167 | if operation == "y" { 168 | if err := cbor.Unmarshal(payloadBytes, &payload.Args.TotalAmountToSkipPotential); err != nil { 169 | return "", nil, err 170 | } 171 | } 172 | return operation, payload, nil 173 | } 174 | return "", nil, errors.ErrOptionNotFound 175 | } 176 | 177 | // parse_atomicals_data_definition_operation 178 | func parseAtomicalsData(script []byte, startIndex int64) ([]byte, error) { 179 | payloadBytes := []byte{} 180 | for startIndex < int64(len(script)) { 181 | op := script[startIndex] 182 | startIndex++ 183 | // define the next instruction type 184 | if op == OP_ENDIF { 185 | break 186 | } 187 | if op <= OP_PUSHDATA4 { 188 | // data, dlen := parsePushData(op, startIndex, script) 189 | data := []byte{} 190 | dlen := int64(0) 191 | if op <= OP_PUSHDATA4 { 192 | // Raw bytes follow 193 | if op < OP_PUSHDATA1 { 194 | dlen = int64(op) 195 | } else if op == OP_PUSHDATA1 { 196 | dlen = int64(script[startIndex]) 197 | startIndex++ 198 | } else if op == OP_PUSHDATA2 { 199 | dlen = int64(utils.UnpackLeUint16From(script[startIndex : startIndex+2])) 200 | startIndex += 2 201 | } else if op == OP_PUSHDATA4 { 202 | dlen = int64(utils.UnpackLeUint32From(script[startIndex : startIndex+4])) 203 | startIndex += 4 204 | } 205 | if int64(startIndex+dlen) > int64(len(script)) { 206 | return nil, errors.ErrInvalidAtomicalsData 207 | } 208 | data = script[startIndex : startIndex+dlen] 209 | startIndex = startIndex + dlen 210 | } 211 | payloadBytes = append(payloadBytes, data...) 212 | } 213 | } 214 | return payloadBytes, nil 215 | } 216 | 217 | const ( 218 | // Constants for Bitcoin Script opcodes 219 | OP_0 = 0 220 | OP_PUSHDATA1 = 76 221 | OP_PUSHDATA2 = 77 222 | OP_PUSHDATA4 = 78 223 | OP_1NEGATE = 79 224 | OP_RESERVED = 80 225 | OP_1 = 81 226 | OP_2 = 82 227 | OP_3 = 83 228 | OP_4 = 84 229 | OP_5 = 85 230 | OP_6 = 86 231 | OP_7 = 87 232 | OP_8 = 88 233 | OP_9 = 89 234 | OP_10 = 90 235 | OP_11 = 91 236 | OP_12 = 92 237 | OP_13 = 93 238 | OP_14 = 94 239 | OP_15 = 95 240 | OP_16 = 96 241 | OP_NOP = 97 242 | OP_VER = 98 243 | OP_IF = 99 244 | OP_NOTIF = 100 245 | OP_VERIF = 101 246 | OP_VERNOTIF = 102 247 | OP_ELSE = 103 248 | OP_ENDIF = 104 249 | OP_VERIFY = 105 250 | OP_RETURN = 106 251 | OP_TOALTSTACK = 107 252 | OP_FROMALTSTACK = 108 253 | OP_2DROP = 109 254 | OP_2DUP = 110 255 | OP_3DUP = 111 256 | OP_2OVER = 112 257 | OP_2ROT = 113 258 | OP_2SWAP = 114 259 | OP_IFDUP = 115 260 | OP_DEPTH = 116 261 | OP_DROP = 117 262 | OP_DUP = 118 263 | OP_NIP = 119 264 | OP_OVER = 120 265 | OP_PICK = 121 266 | OP_ROLL = 122 267 | OP_ROT = 123 268 | OP_SWAP = 124 269 | OP_TUCK = 125 270 | OP_CAT = 126 271 | OP_SUBSTR = 127 272 | OP_LEFT = 128 273 | OP_RIGHT = 129 274 | OP_SIZE = 130 275 | OP_INVERT = 131 276 | OP_AND = 132 277 | OP_OR = 133 278 | OP_XOR = 134 279 | OP_EQUAL = 135 280 | OP_EQUALVERIFY = 136 281 | OP_RESERVED1 = 137 282 | OP_RESERVED2 = 138 283 | OP_1ADD = 139 284 | OP_1SUB = 140 285 | OP_2MUL = 141 286 | OP_2DIV = 142 287 | OP_NEGATE = 143 288 | OP_ABS = 144 289 | OP_NOT = 145 290 | OP_0NOTEQUAL = 146 291 | OP_ADD = 147 292 | OP_SUB = 148 293 | OP_MUL = 149 294 | OP_DIV = 150 295 | OP_MOD = 151 296 | OP_LSHIFT = 152 297 | OP_RSHIFT = 153 298 | OP_BOOLAND = 154 299 | OP_BOOLOR = 155 300 | OP_NUMEQUAL = 156 301 | OP_NUMEQUALVERIFY = 157 302 | OP_NUMNOTEQUAL = 158 303 | OP_LESSTHAN = 159 304 | OP_GREATERTHAN = 160 305 | OP_LESSTHANOREQUAL = 161 306 | OP_GREATERTHANOREQUAL = 162 307 | OP_MIN = 163 308 | OP_MAX = 164 309 | OP_WITHIN = 165 310 | OP_RIPEMD160 = 166 311 | OP_SHA1 = 167 312 | OP_SHA256 = 168 313 | OP_HASH160 = 169 314 | OP_HASH256 = 170 315 | OP_CODESEPARATOR = 171 316 | OP_CHECKSIG = 172 317 | OP_CHECKSIGVERIFY = 173 318 | OP_CHECKMULTISIG = 174 319 | OP_CHECKMULTISIGVERIFY = 175 320 | OP_NOP1 = 176 321 | OP_CHECKLOCKTIMEVERIFY = 177 322 | OP_CHECKSEQUENCEVERIFY = 178 323 | ) 324 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/python-parse/api.go: -------------------------------------------------------------------------------- 1 | package pythonparse 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | // parse_operation_from_script 10 | func ParseAtomicalsOperation(script string, height int64) (bool, error) { 11 | cmd := exec.Command("python3", "atomicals-core/witness/python-parse/parse.py", script, fmt.Sprintf("%d", height)) 12 | output, err := cmd.CombinedOutput() 13 | if err != nil { 14 | return false, err 15 | } 16 | isValid, err := parseOperation(string(output)) 17 | if err != nil { 18 | return false, err 19 | } 20 | if isValid == "" { 21 | return true, nil 22 | } 23 | return false, nil 24 | } 25 | 26 | func parseOperation(input string) (string, error) { 27 | // Split the input string by spaces 28 | parts := strings.Split(input, "\n") 29 | 30 | // Check if we have enough parts and the correct format 31 | if len(parts) == 1 { 32 | // Return the last part, which should be "nft" 33 | return parts[0], nil 34 | } 35 | 36 | // If the format doesn't match, return an error 37 | return "", fmt.Errorf("invalid input format") 38 | } 39 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/python-parse/enumeration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Neil Booth 2 | # 3 | # All rights reserved. 4 | # 5 | # See the file "LICENCE" for information about the copyright 6 | # and warranty status of this software. 7 | 8 | '''An enum-like type with reverse lookup. 9 | 10 | Source: Python Cookbook, http://code.activestate.com/recipes/67107/ 11 | ''' 12 | 13 | 14 | class EnumError(Exception): 15 | pass 16 | 17 | 18 | class Enumeration: 19 | 20 | def __init__(self, name, enumList): 21 | self.__doc__ = name 22 | 23 | lookup = {} 24 | reverseLookup = {} 25 | i = 0 26 | uniqueNames = set() 27 | uniqueValues = set() 28 | for x in enumList: 29 | if isinstance(x, tuple): 30 | x, i = x 31 | if not isinstance(x, str): 32 | raise EnumError("enum name {} not a string".format(x)) 33 | if not isinstance(i, int): 34 | raise EnumError("enum value {} not an integer".format(i)) 35 | if x in uniqueNames: 36 | raise EnumError("enum name {} not unique".format(x)) 37 | if i in uniqueValues: 38 | raise EnumError("enum value {} not unique".format(x)) 39 | uniqueNames.add(x) 40 | uniqueValues.add(i) 41 | lookup[x] = i 42 | reverseLookup[i] = x 43 | i = i + 1 44 | self.lookup = lookup 45 | self.reverseLookup = reverseLookup 46 | 47 | def __getattr__(self, attr): 48 | result = self.lookup.get(attr) 49 | if result is None: 50 | raise AttributeError('enumeration has no member {}'.format(attr)) 51 | return result 52 | 53 | def whatis(self, value): 54 | return self.reverseLookup[value] 55 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/python-parse/parse.py: -------------------------------------------------------------------------------- 1 | # script.py 2 | 3 | from cbor2 import loads 4 | from util import parse_protocols_operations_from_witness_for_input,is_sanitized_dict_whitelist_only,is_density_activated 5 | import sys 6 | 7 | def parse(witness_script, height): 8 | op_name, payload = parse_protocols_operations_from_witness_for_input([bytes.fromhex(witness_script)]) 9 | if not op_name: 10 | return 11 | decoded_object = {} 12 | if payload: 13 | # Ensure that the payload is cbor encoded dictionary or empty 14 | try: 15 | decoded_object = loads(payload) 16 | if not isinstance(decoded_object, dict): 17 | print("false") 18 | return 19 | except Exception as e: 20 | print("false") 21 | return 22 | # Also enforce that if there are meta, args, or ctx fields that they must be dicts 23 | # This is done to ensure that these fields are always easily parseable and do not contain unexpected data which could cause parsing problems later 24 | # Ensure that they are not allowed to contain bytes like objects 25 | if ( 26 | not is_sanitized_dict_whitelist_only(decoded_object.get("meta", {})) 27 | or not is_sanitized_dict_whitelist_only(decoded_object.get("args", {}), is_density_activated(height)) 28 | or not is_sanitized_dict_whitelist_only(decoded_object.get("ctx", {})) 29 | or not is_sanitized_dict_whitelist_only(decoded_object.get("init", {}), True) 30 | ): 31 | print("false") 32 | return 33 | else: 34 | print("false") 35 | 36 | if __name__ == "__main__": 37 | if len(sys.argv) != 3: 38 | print("Usage: python script.py ") 39 | sys.exit(1) 40 | witness_script = sys.argv[1] 41 | height = int(sys.argv[2]) # Convert height to integer 42 | parse(witness_script,height) 43 | -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/python-parse/util.py: -------------------------------------------------------------------------------- 1 | from enumeration import Enumeration 2 | from struct import Struct 3 | import sys 4 | 5 | struct_le_i = Struct('Q') 11 | struct_be_H = Struct('>H') 12 | struct_be_I = Struct('>I') 13 | structB = Struct('B') 14 | 15 | unpack_le_int32_from = struct_le_i.unpack_from 16 | unpack_le_int64_from = struct_le_q.unpack_from 17 | unpack_le_uint16_from = struct_le_H.unpack_from 18 | unpack_le_uint32_from = struct_le_I.unpack_from 19 | unpack_le_uint64_from = struct_le_Q.unpack_from 20 | unpack_be_uint16_from = struct_be_H.unpack_from 21 | unpack_be_uint32_from = struct_be_I.unpack_from 22 | 23 | unpack_le_uint32 = struct_le_I.unpack 24 | unpack_le_uint64 = struct_le_Q.unpack 25 | unpack_be_uint64 = struct_be_Q.unpack 26 | unpack_be_uint32 = struct_be_I.unpack 27 | 28 | pack_le_int32 = struct_le_i.pack 29 | pack_le_int64 = struct_le_q.pack 30 | pack_le_uint16 = struct_le_H.pack 31 | pack_le_uint32 = struct_le_I.pack 32 | pack_le_uint64 = struct_le_Q.pack 33 | pack_be_uint64 = struct_be_Q.pack 34 | pack_be_uint16 = struct_be_H.pack 35 | pack_be_uint32 = struct_be_I.pack 36 | pack_byte = structB.pack 37 | 38 | hex_to_bytes = bytes.fromhex 39 | 40 | # Parses and detects valid Atomicals protocol operations in a witness script 41 | # Stops when it finds the first operation in the first input 42 | def parse_protocols_operations_from_witness_for_input(txinwitness): 43 | '''Detect and parse all operations across the witness input arrays from a tx''' 44 | atomical_operation_type_map = {} 45 | for script in txinwitness: 46 | n = 0 47 | script_entry_len = len(script) 48 | if script_entry_len < 39 or script[0] != 0x20: 49 | continue 50 | found_operation_definition = False 51 | while n < script_entry_len - 5: 52 | op = script[n] 53 | n += 1 54 | # Match the pubkeyhash 55 | if op == 0x20 and n + 32 <= script_entry_len: 56 | n = n + 32 57 | while n < script_entry_len - 5: 58 | op = script[n] 59 | n += 1 60 | # Get the next if statement 61 | if op == OpCodes.OP_IF: 62 | if ATOMICALS_ENVELOPE_MARKER_BYTES == script[n : n + 5].hex(): 63 | found_operation_definition = True 64 | # Parse to ensure it is in the right format 65 | operation_type, payload = parse_operation_from_script(script, n + 5) 66 | if operation_type != None: 67 | return operation_type, payload 68 | break 69 | if found_operation_definition: 70 | break 71 | else: 72 | break 73 | return None, None 74 | 75 | 76 | # Parses the valid operations in an Atomicals script 77 | def parse_operation_from_script(script, n): 78 | '''Parse an operation''' 79 | # Check for each protocol operation 80 | script_len = len(script) 81 | atom_op_decoded = None 82 | one_letter_op_len = 2 83 | two_letter_op_len = 3 84 | three_letter_op_len = 4 85 | 86 | # check the 3 letter protocol operations 87 | if n + three_letter_op_len < script_len: 88 | atom_op = script[n : n + three_letter_op_len].hex() 89 | if atom_op == "036e6674": 90 | atom_op_decoded = 'nft' # nft - Mint non-fungible token 91 | elif atom_op == "03646674": 92 | atom_op_decoded = 'dft' # dft - Deploy distributed mint fungible token starting point 93 | elif atom_op == "036d6f64": 94 | atom_op_decoded = 'mod' # mod - Modify general state 95 | elif atom_op == "03657674": 96 | atom_op_decoded = 'evt' # evt - Message response/reply 97 | elif atom_op == "03646d74": 98 | atom_op_decoded = 'dmt' # dmt - Mint tokens of distributed mint type (dft) 99 | elif atom_op == "03646174": 100 | atom_op_decoded = 'dat' # dat - Store data on a transaction (dat) 101 | if atom_op_decoded: 102 | return atom_op_decoded, parse_atomicals_data_definition_operation(script, n + three_letter_op_len) 103 | 104 | # check the 2 letter protocol operations 105 | if n + two_letter_op_len < script_len: 106 | atom_op = script[n : n + two_letter_op_len].hex() 107 | if atom_op == "026674": 108 | atom_op_decoded = 'ft' # ft - Mint fungible token with direct fixed supply 109 | elif atom_op == "02736c": 110 | atom_op_decoded = 'sl' # sl - Seal an NFT and lock it from further changes forever 111 | 112 | if atom_op_decoded: 113 | return atom_op_decoded, parse_atomicals_data_definition_operation(script, n + two_letter_op_len) 114 | 115 | # check the 1 letter 116 | if n + one_letter_op_len < script_len: 117 | atom_op = script[n : n + one_letter_op_len].hex() 118 | # Extract operation (for NFTs only) 119 | if atom_op == "0178": 120 | atom_op_decoded = 'x' # extract - move atomical to 0'th output 121 | elif atom_op == "0179": 122 | atom_op_decoded = 'y' # split - 123 | 124 | if atom_op_decoded: 125 | return atom_op_decoded, parse_atomicals_data_definition_operation(script, n + one_letter_op_len) 126 | 127 | # print(f'Invalid Atomicals Operation Code. Skipping... "{script[n : n + 4].hex()}"') 128 | return None, None 129 | 130 | 131 | # Parses all of the push datas in a script and then concats/accumulates the bytes together 132 | # It allows the encoding of a multi-push binary data across many pushes 133 | def parse_atomicals_data_definition_operation(script, n): 134 | '''Extract the payload definitions''' 135 | accumulated_encoded_bytes = b'' 136 | try: 137 | script_entry_len = len(script) 138 | while n < script_entry_len: 139 | op = script[n] 140 | n += 1 141 | # define the next instruction type 142 | if op == OpCodes.OP_ENDIF: 143 | break 144 | elif op <= OpCodes.OP_PUSHDATA4: 145 | data, n, dlen = parse_push_data(op, n, script) 146 | accumulated_encoded_bytes = accumulated_encoded_bytes + data 147 | return accumulated_encoded_bytes 148 | except Exception as e: 149 | raise ScriptError(f'parse_atomicals_data_definition_operation script error {e}') from None 150 | 151 | # Parses the push datas from a bitcoin script byte sequence 152 | def parse_push_data(op, n, script): 153 | data = b'' 154 | if op <= OpCodes.OP_PUSHDATA4: 155 | # Raw bytes follow 156 | if op < OpCodes.OP_PUSHDATA1: 157 | dlen = op 158 | elif op == OpCodes.OP_PUSHDATA1: 159 | dlen = script[n] 160 | n += 1 161 | elif op == OpCodes.OP_PUSHDATA2: 162 | dlen, = unpack_le_uint16_from(script[n: n + 2]) 163 | n += 2 164 | elif op == OpCodes.OP_PUSHDATA4: 165 | dlen, = unpack_le_uint32_from(script[n: n + 4]) 166 | n += 4 167 | if n + dlen > len(script): 168 | raise IndexError 169 | data = script[n : n + dlen] 170 | return data, n + dlen, dlen 171 | 172 | 173 | OpCodes = Enumeration("Opcodes", [ 174 | ("OP_0", 0), ("OP_PUSHDATA1", 76), 175 | "OP_PUSHDATA2", "OP_PUSHDATA4", "OP_1NEGATE", 176 | "OP_RESERVED", 177 | "OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6", "OP_7", "OP_8", 178 | "OP_9", "OP_10", "OP_11", "OP_12", "OP_13", "OP_14", "OP_15", "OP_16", 179 | "OP_NOP", "OP_VER", "OP_IF", "OP_NOTIF", "OP_VERIF", "OP_VERNOTIF", 180 | "OP_ELSE", "OP_ENDIF", "OP_VERIFY", "OP_RETURN", 181 | "OP_TOALTSTACK", "OP_FROMALTSTACK", "OP_2DROP", "OP_2DUP", "OP_3DUP", 182 | "OP_2OVER", "OP_2ROT", "OP_2SWAP", "OP_IFDUP", "OP_DEPTH", "OP_DROP", 183 | "OP_DUP", "OP_NIP", "OP_OVER", "OP_PICK", "OP_ROLL", "OP_ROT", 184 | "OP_SWAP", "OP_TUCK", 185 | "OP_CAT", "OP_SUBSTR", "OP_LEFT", "OP_RIGHT", "OP_SIZE", 186 | "OP_INVERT", "OP_AND", "OP_OR", "OP_XOR", "OP_EQUAL", "OP_EQUALVERIFY", 187 | "OP_RESERVED1", "OP_RESERVED2", 188 | "OP_1ADD", "OP_1SUB", "OP_2MUL", "OP_2DIV", "OP_NEGATE", "OP_ABS", 189 | "OP_NOT", "OP_0NOTEQUAL", "OP_ADD", "OP_SUB", "OP_MUL", "OP_DIV", "OP_MOD", 190 | "OP_LSHIFT", "OP_RSHIFT", "OP_BOOLAND", "OP_BOOLOR", "OP_NUMEQUAL", 191 | "OP_NUMEQUALVERIFY", "OP_NUMNOTEQUAL", "OP_LESSTHAN", "OP_GREATERTHAN", 192 | "OP_LESSTHANOREQUAL", "OP_GREATERTHANOREQUAL", "OP_MIN", "OP_MAX", 193 | "OP_WITHIN", 194 | "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160", "OP_HASH256", 195 | "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG", 196 | "OP_CHECKMULTISIGVERIFY", 197 | "OP_NOP1", 198 | "OP_CHECKLOCKTIMEVERIFY", "OP_CHECKSEQUENCEVERIFY" 199 | ]) 200 | 201 | # The maximum height difference between the commit and reveal transactions of any Atomical mint 202 | # This is used to limit the amount of cache we would need in future optimizations. 203 | MINT_GENERAL_COMMIT_REVEAL_DELAY_BLOCKS = 100 204 | 205 | # The maximum height difference between the commit and reveal transactions of any of the sub(realm) mints 206 | # This is needed to prevent front-running of realms. 207 | MINT_REALM_CONTAINER_TICKER_COMMIT_REVEAL_DELAY_BLOCKS = 3 208 | 209 | MINT_SUBNAME_RULES_BECOME_EFFECTIVE_IN_BLOCKS = 1 # magic number that rules become effective in one block 210 | 211 | # The path namespace to look for when determining what price/regex patterns are allowed if any 212 | SUBREALM_MINT_PATH = 'subrealms' 213 | 214 | # The path namespace to look for when determining what price/regex patterns are allowed if any 215 | DMINT_PATH = 'dmint' 216 | 217 | # The maximum height difference between the reveal transaction of the winning subrealm claim and the blocks to pay the necessary fee to the parent realm 218 | # It is intentionally made longer since it may take some time for the purchaser to get the funds together 219 | MINT_SUBNAME_COMMIT_PAYMENT_DELAY_BLOCKS = 15 # ~2.5 hours. 220 | # MINT_REALM_CONTAINER_TICKER_COMMIT_REVEAL_DELAY_BLOCKS and therefore it gives the user about 1.5 hours to make the payment after they know 221 | # that they won the realm (and no one else can claim/reveal) 222 | 223 | # The Envelope is for the reveal script and also the op_return payment markers 224 | # "atom" 225 | ATOMICALS_ENVELOPE_MARKER_BYTES = '0461746f6d' 226 | 227 | # Limit the smallest payment amount allowed for a subrealm 228 | SUBNAME_MIN_PAYMENT_DUST_LIMIT = 0 # It can be possible to do free 229 | 230 | # Maximum size of the rules of a subrealm or container dmint rule set array 231 | MAX_SUBNAME_RULE_SIZE_LEN = 100000 232 | # Maximum number of minting rules allowed 233 | MAX_SUBNAME_RULE_ENTRIES = 100 234 | 235 | # Minimum amount in satoshis for a DFT mint operation. Set at satoshi dust of 546 236 | DFT_MINT_AMOUNT_MIN = 546 237 | 238 | # Maximum amount in satoshis for the DFT mint operation. Set at 1 BTC for the ballers 239 | DFT_MINT_AMOUNT_MAX = 100000000 240 | 241 | # The minimum number of DFT max_mints. Set at 1 242 | DFT_MINT_MAX_MIN_COUNT = 1 243 | # The maximum number (legacy) of DFT max_mints. Set at 500,000 mints mainly for efficieny reasons in legacy. 244 | DFT_MINT_MAX_MAX_COUNT_LEGACY = 500000 245 | # The maximum number of DFT max_mints (after legacy 'DENSITY' update). Set at 21,000,000 max mints. 246 | DFT_MINT_MAX_MAX_COUNT_DENSITY = 21000000 247 | 248 | # This would never change, but we put it as a constant for clarity 249 | DFT_MINT_HEIGHT_MIN = 0 250 | # This value would never change, it's added in case someone accidentally tries to use a unixtime 251 | DFT_MINT_HEIGHT_MAX = 10000000 # 10 million blocks 252 | 253 | def is_sanitized_dict_whitelist_only(d: dict, allow_bytes=False): 254 | if not isinstance(d, dict): 255 | return False 256 | for k, v in d.items(): 257 | if isinstance(v, dict): 258 | return is_sanitized_dict_whitelist_only(v, allow_bytes) 259 | if not allow_bytes and isinstance(v, bytes): 260 | # print( f"parse {k} {v} ..." ) 261 | return False 262 | if ( 263 | not isinstance(v, int) 264 | and not isinstance(v, float) 265 | and not isinstance(v, list) 266 | and not isinstance(v, str) 267 | and not isinstance(v, bytes) 268 | ): 269 | # Prohibit anything except int, float, lists, strings and bytes 270 | return False 271 | return True 272 | 273 | def is_density_activated(height: int): 274 | ATOMICALS_ACTIVATION_HEIGHT_DENSITY = 828128 275 | if height >= ATOMICALS_ACTIVATION_HEIGHT_DENSITY: 276 | return True 277 | return False -------------------------------------------------------------------------------- /atomicals-indexer/atomicals-core/witness/witness.go: -------------------------------------------------------------------------------- 1 | package witness 2 | 3 | import ( 4 | pythonparse "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness/python-parse" 5 | "github.com/atomicals-go/utils" 6 | 7 | "github.com/btcsuite/btcd/btcjson" 8 | ) 9 | 10 | type WitnessAtomicalsOperation struct { 11 | Script string 12 | Op string 13 | Payload *PayLoad 14 | 15 | CommitTxID string // vin's txID 16 | CommitVoutIndex int64 // vin's index as vout in last tx 17 | CommitHeight int64 18 | CommitTxIndex int64 19 | 20 | RevealLocationTxID string 21 | RevealInputIndex int64 22 | RevealLocationHeight int64 23 | 24 | AtomicalsID string 25 | LocationID string 26 | } 27 | 28 | // # Parses and detects valid Atomicals protocol operations in a witness script 29 | // # Stops when it finds the first operation in the first input 30 | func ParseWitness(tx btcjson.TxRawResult, height int64) *WitnessAtomicalsOperation { 31 | for vinIndex, vin := range tx.Vin { 32 | if !vin.HasWitness() { 33 | continue 34 | } 35 | for _, script := range vin.Witness { 36 | op, payload, err := parseOperationAndPayLoad(script, height) 37 | if err != nil { 38 | continue 39 | } 40 | if op != "" { 41 | isValid, err := pythonparse.ParseAtomicalsOperation(script, height) 42 | if err != nil { 43 | continue 44 | } 45 | if !isValid { 46 | continue 47 | } 48 | } 49 | return &WitnessAtomicalsOperation{ 50 | Op: op, 51 | Payload: payload, 52 | Script: script, 53 | CommitTxID: vin.Txid, 54 | CommitVoutIndex: int64(vin.Vout), 55 | CommitHeight: -1, 56 | CommitTxIndex: -1, 57 | 58 | AtomicalsID: utils.AtomicalsID(vin.Txid, int64(vin.Vout)), 59 | LocationID: utils.AtomicalsID(tx.Txid, int64(vinIndex)), 60 | RevealLocationTxID: tx.Txid, 61 | RevealInputIndex: int64(vinIndex), 62 | RevealLocationHeight: height, 63 | } 64 | } 65 | break 66 | } 67 | return &WitnessAtomicalsOperation{ 68 | RevealLocationTxID: tx.Txid, 69 | RevealInputIndex: -1, 70 | RevealLocationHeight: height, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /atomicals-indexer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | 8 | atomicals "github.com/atomicals-go/atomicals-indexer/atomicals-core/operation" 9 | "github.com/atomicals-go/pkg/conf" 10 | ) 11 | 12 | func main() { 13 | // use a port for single-mode 14 | port := "8080" 15 | ln, err := net.Listen("tcp", ":"+port) 16 | if err != nil { 17 | fmt.Printf("Error:%v, Cannot obtain port %s, Atomicals-Core is probably already running.\n", err, port) 18 | os.Exit(1) 19 | } 20 | defer ln.Close() 21 | conf, err := conf.ReadJSONFromJSFile("../conf/config.json") 22 | if err != nil { 23 | panic(err) 24 | } 25 | a := atomicals.NewAtomicalsWithSQL(conf) 26 | for { 27 | a.Run() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /conf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "btc_rpc_url" : "0.0.0.0:8332", 3 | "btc_rpc_user": "btc" , 4 | "btc_rpc_password": "btc2012", 5 | "sql_dns": "host=127.0.0.1 user=admin password=admin123 dbname=atomicals port=5432 sslmode=disable", 6 | "atomicals_start_height": 812481 7 | } -------------------------------------------------------------------------------- /conf/configfractalbitcoin.json: -------------------------------------------------------------------------------- 1 | { 2 | "btc_rpc_url" : "0.0.0.0:8332", 3 | "btc_rpc_user": "fractalbitcoin" , 4 | "btc_rpc_password": "fractalbitcoin_1234567", 5 | "sql_dns": "host=127.0.0.1 user=admin password=admin123 dbname=atomicals port=5432 sslmode=disable", 6 | "atomicals_start_height": 0 7 | } -------------------------------------------------------------------------------- /doc/0.atomicalsCoreFramework.md: -------------------------------------------------------------------------------- 1 | ## atomicals-core Framework 2 | 3 | ### core work 4 | atomicals-core的核心工作是维护一个记录用户atomicals资产的账本,它可以有不同的存储载体:计算机内存,redis或者sql 5 | - 目前的版本中我提供了存储于sql的版本atomicals/DB/postgres.go 6 | - 如果您想使用其他存储方式,只需要提供atomicals/DB/api.go中定义的全部接口,在NewDB中返回您重新实现的结构体即可 7 | 8 | atomicals-core维护的两个账本 9 | - UTXOFtInfo:记录用户铸造的全部nft信息(包括realm) 10 | - 当用户铸造nft时,会对一个UTXO染色,UserNftInfo记录了染色信息。 11 | - AtomicalsID为用户铸造nft时,commit tx的txid+txhash, 你可以在atomicals/utils/utils.go/AtomicalsID()中看到它是如何生成的 12 | - LocationID为nft被transfer时,新染色的commit tx的txid+txhash 13 | 14 | - UTXONftInfo:记录用户铸造的全部ft信息 15 | 16 | ### operation 17 | - 在atomicals/witness中实现了对btc交易witness字段的解析。若atomicals协议有新的拓展命令,只需要更改此package 18 | - 在atomicals/目录下以operation*.go 为前缀的go文件为对不同atomicals命令的处理函数 19 | 20 | ### transfer 21 | - atomicals 染色过的utxo被transfer时,会将此utxo销毁,对tx vout重新染色,以此完成atomicals资产的转移 22 | 23 | ### main logic 24 | - atomicals/trace.go是atomicals-core的主逻辑,在处理一笔交易时,atomicals索引器的处理顺序是 25 | - 检查交易vin中是否有atomicals资产,若有,执行transfer逻辑 26 | - 检查是否有atomicals operation,并执行对应处理函数 27 | - 检查是否有payment 28 | 29 | ### concurrency and caching 30 | - pkg/btcsync/block.go 使用通道并行获取block,即使您使用远程btc node,在atomicals-core处理block时,提前获取下一个block 31 | - pkg/btcsync/txHeightCache.go 维护最近100个block的tx-blockHeight缓存,减少GetCommitHeight请求时间 32 | - atomicals/DB/db.go RealmCache ContainerCache 用户判断realm container是否已存在 33 | -------------------------------------------------------------------------------- /doc/1.utxoColor.md: -------------------------------------------------------------------------------- 1 | 2 | ## 如何对UTXO染色并发行代币 3 | 4 | ## UTXO 5 | 一笔BTC交易包含的详细字段如下: 6 | 7 | ``` 8 | type TxRawResult struct { 9 | Hex string `json:"hex"` 10 | Txid string `json:"txid"` 11 | Hash string `json:"hash,omitempty"` 12 | Size int32 `json:"size,omitempty"` 13 | Vsize int32 `json:"vsize,omitempty"` 14 | Weight int32 `json:"weight,omitempty"` 15 | Version uint32 `json:"version"` 16 | LockTime uint32 `json:"locktime"` 17 | Vin []Vin `json:"vin"` 18 | Vout []Vout `json:"vout"` 19 | BlockHash string `json:"blockhash,omitempty"` 20 | Confirmations uint64 `json:"confirmations,omitempty"` 21 | Time int64 `json:"time,omitempty"` 22 | Blocktime int64 `json:"blocktime,omitempty"` 23 | } 24 | ``` 25 | 26 | btc一笔交易TxRawResult中包含若干个Vout(UTXO,unspent transaction output)和若干个Vin。Vin是上一笔交易的UTXO,但是在这笔交易中,它变成了TXO(spent) 27 | 28 | [一笔真实的BTC交易](https://mempool.space/zh/tx/00425a7ef7e3387efcf754c6df2e037025f15b5b1b00bcac1429cb49a3a17353)是这样的: 29 | 30 | ![一笔真实的BTC交易](https://github.com/yimingWOW/atomicals-indexer/atomicals-core/tree/main/doc/pic/uxto.png) 31 | 32 | 33 | ## 如何给UTXO做标记 34 | 假如yiming,Alice和Bob之间发生了2笔转账: 35 | 36 | - yiming send 1BTC to Alice (PS:yiming真的没这么多BTC ~_~ ) 37 | - Balance: 38 | - yiming:0BTC Alice:0.99999BTC Bob:0BTC 39 | ``` 40 | tx{ 41 | TxID = "56fe36d4c94d04dbb259b00bd06dfb85ge98212bb3d543eec2b9c6f5ge901b23" 42 | Vin[0] // 包含0.3BTC 43 | Vin[1] // 包含0.7BTC 44 | Vout[0] // 0.99999BTC, 该Vout记录了0.99999BTC到了Alice账下 45 | } 46 | ``` 47 | 48 | - Alice send 1BTC to Bob 49 | - Balance: 50 | - yiming:0BTC, Alice:0.499BTC Bob:0.5BTC 51 | ``` 52 | tx{ 53 | TxID = "160k76d4c94d04dbb259b00bd06dfb85e83d8212bb3d54eec2b9c6f501b2e83d" 54 | Vin[0] // 包含0.3BTC 55 | Vin[1] // 包含0.7BTC 56 | Vout[0] // 0.5BTC, 该Vout记录了0.5BTC到了Bob账下 57 | Vout[0] // 0.499BTC, 该Vout记录了0.499BTC到了Alice账下 58 | } 59 | ``` 60 | 61 | 62 | yiming的Satoshi经过2笔交易后到了Alice,Bob和矿工账下,我们可以发现曾经被yiming拥有的每一个Satoshi(btc的最小单位)的流向都可以被定位。 63 | 64 | - Bob最后拥有的Satoshi曾经位于: 65 | - TxID = "56fe36d4c94d04dbb259b00bd06dfb85ge98212bb3d543eec2b9c6f5ge901b23" 66 | - VoutIndex = 0 67 | - 随后到了: 68 | - TxID = "160k76d4c94d04dbb259b00bd06dfb85e83d8212bb3d54eec2b9c6f501b2e83d" 69 | - VoutIndex = 0 70 | - 而Alice拥有的Satoshi曾经位于: 71 | - TxID = "56fe36d4c94d04dbb259b00bd06dfb85ge98212bb3d543eec2b9c6f5ge901b23" 72 | - VoutIndex = 0 73 | - 随后到了: 74 | - TxID = "160k76d4c94d04dbb259b00bd06dfb85e83d8212bb3d54eec2b9c6f501b2e83d" 75 | - VoutIndex = 1 76 | 77 | 78 | 每个新的Block产生,矿工收获新的BTC都会以UTXO的形式记在他的账下。考古一下,btc的[第一笔交易](https://mempool.space/zh/tx/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b)挖出了50枚比特币。如果我们把每个Satoshi按照诞生顺序编号的话,此时有50*100000000个Satoshi,他们的序号分别是0~(50*100000000-1),也许yiming刚刚转账的1BTC中就包含50*100000000个Satoshi中的一部份呢 79 | 80 | 但是在编程时,我们并不需要为这些Satoshi开辟内存空间存储他们的序号。如果这样做需要在程序运行之前就开辟一个长度为2100w*100000000的数组,显然和占用内存空间。每个Satoshi所在的位置可以用TxID和VoutIndex表示,这样足够我们使用了。 81 | 82 | ## 用UTXO染色的方式发行资产 83 | Satoshi可以被定位为BTC上资产发行提供了一个途径。如果yiming曾经拥有过的1*100000000个Satoshi被赋予了某种特别的意义,比如说持有这些Satoshi的人可以免费来我家摸我的猫一次? 84 | 这些Satoshi似乎有了额外的价值,当然前提是yiming始终兑现该承诺,并且yiming的猫始终愿意让别人来摸,并且您愿意来摸。资产赋能的故事留给其他人去讲吧,我们继续说下atomicals如何实现资产发行的。 85 | 86 | 每笔BTC交易的每个Vin中都包含witness字段,如图: 87 | 88 | ![交易Vin中的witness字段](https://github.com/yimingWOW/atomicals-indexer/atomicals-core/tree/main/doc/pic/uxtoDetails.png) 89 | 90 | atomicals协议利用witness字段将UTXO做标记(染色),我举一个略显牵强的例子: 91 | 92 | - yiming发了一笔新的交易(刚刚有好心的给yiming转账了,所以他有了新的BTC继续为大家举例子),这比交易中在Vin[0].Witness字段中写道: 93 | 94 | ``` 95 | tx{ 96 | TxID = "3eeb6f5ge901b2396f4d259bb85ge98212be36dc200bd06dfb9c04db4c9b3d54" 97 | Vin[0] = { 98 | witness = { 99 | optionType:"deploy" 100 | tokenName: "撸猫专用" 101 | maxSupply: 21000w 102 | maxMintAmount: 1000 103 | } 104 | } 105 | Vout[0] 106 | } 107 | ``` 108 | 109 | - yiming部署了一个名为"撸猫专用"的代币! 110 | 111 | - 随后Alice发了一笔新的交易给自己转账1000Satoshi,这比交易中在Vin[0].Witness字段中写道: 112 | ``` 113 | tx{ 114 | TxID = "396f4d259bb85ge98212be36dc200bd06dfb9c04db4c9b3d543eeb6f5ge901b2" 115 | Vin[0] { 116 | witness = { 117 | optionType:"mint" 118 | tokenName: "撸猫专用" 119 | } 120 | } 121 | Vout[0] 122 | } 123 | ``` 124 | 125 | - 我们约定好每笔交易中的Vin的witness字段中如果包含合法的mint语句,那么该交易就铸造了一个新的代币。Vout[0].ScriptPubKey为代币的所有者,Vout[0].Value就是这个人mint的新的代币的数量。 126 | - 刚刚Alice为自己mint了1000个"撸猫专用"代币。 127 | 128 | - 随后Alice又发了一笔转账交易,转1000Satoshi给Bob(Alice什么老给Bob转账?!!) 129 | ``` 130 | tx{ 131 | TxID = "396f4d259bb85ge98212be36dc200bd06dfb9c04db4c9b3d543eeb6f5ge901b2" 132 | Vin[0] { 133 | TxID = "396f4d259bb85ge98212be36dc200bd06dfb9c04db4c9b3d543eeb6f5ge901b2" 134 | VoutIndex = 0 135 | witness = { 136 | optionType:"mint" 137 | tokenName: "撸猫专用" 138 | } 139 | } 140 | Vout[0] 141 | } 142 | ``` 143 | - 各位发现了吗?该交易的Vin[0]来自Alice刚刚发的那笔交易,因为Vin[0]的 144 | - TxID = "396f4d259bb85ge98212be36dc200bd06dfb9c04db4c9b3d543eeb6f5ge901b2" 145 | - VoutIndex = 0 146 | - Alice转账给Bob后,这1000个"撸猫专用"代币和1000Satoshi一并归Bob所有了 147 | 148 | 写到这里,你们一定知道Atomicals的代币部署和铸造流程了,接下来我们看看协议具体内容吧。 149 | -------------------------------------------------------------------------------- /doc/2.atomicalsProtocal.md: -------------------------------------------------------------------------------- 1 | # Atomicals 协议 2 | 3 | 我认为atomicals协议包含两部分:1.链上atomicals命令解析器;2.indexer检查条件 4 | 5 | 6 | ## 链上atomicals命令解析器 7 | 8 | #### 解析器代码 9 | 10 | - atomicals-electrumx中的witness字段解析代码在这里:[parse_protocols_operations_from_witness_for_input()](https://github.com/atomicals/atomicals-electrumx/blob/a70089d9d62ed4e3c4af0effbc74eb715c84bca2/electrumx/lib/util_atomicals.py#L1162),atomicals-core的witness字段解析代码在 [ParseOperationAndPayLoad()](https://github.com/yimingWOW/atomicals-indexer/atomicals-core/blob/main/atomicals/witness/witness.go#63) 11 | 12 | - 您可以下载atomicals-electrumx代码并新建一个.py文件运行如下代码 13 | 14 | ``` 15 | from cbor2 import loads 16 | from electrumx.lib.util_atomicals import parse_protocols_operations_from_witness_for_input 17 | witenss_script = "2069006ea562e243388d8a8737c8ccb9bd9bacb7c33775a772bc04f0a19d6b0b57ac00630461746f6d03646d743ba16461726773a468626974776f726b6364313631386b6d696e745f7469636b65726461746f6d656e6f6e63651a005319fa6474696d651a650a66a568" 18 | op_name, payload = parse_protocols_operations_from_witness_for_input([bytes.fromhex(witenss_script)]) 19 | decoded_object = {} 20 | decoded_object = loads(payload) 21 | print("op_name:",op_name) 22 | print("decoded_object:",decoded_object) 23 | ``` 24 | - 以上代码的执行结果,是某次dmt铸造atom: 25 | 26 | ``` 27 | op_name: dmt 28 | decoded_object: {'args': {'bitworkc': '1618', 'mint_ticker': 'atom', 'nonce': 5446138, 'time': 1695180453}} 29 | ``` 30 | 31 | - 想要验证二者是否一致,可以下载atomicals-core并新建一个.go文件运行如下代码: 32 | 33 | ``` 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | 39 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 40 | ) 41 | func main() { 42 | op, payload, err := witness.ParseOperationAndPayLoad("2069006ea562e243388d8a8737c8ccb9bd9bacb7c33775a772bc04f0a19d6b0b57ac00630461746f6d03646d743ba16461726773a468626974776f726b6364313631386b6d696e745f7469636b65726461746f6d656e6f6e63651a005319fa6474696d651a650a66a568") 43 | if err != nil { 44 | panic(err) 45 | } 46 | fmt.Printf("op:%+v", op) 47 | fmt.Printf("payload:%+v", payload) 48 | } 49 | ``` 50 | 51 | #### operationType 52 | 53 | 目前atomicals协议中对witness中的命令operationType有10种: 54 | 55 | - dft - Deploy distributed mint fungible token starting point 56 | - dmt - Mint tokens of distributed mint type (dft) 57 | - nft - Mint non-fungible token 58 | - ft - Mint fungible token with direct fixed supply 59 | - mod - Modify general state 60 | - evt - Message response/reply 61 | - dat - Store data on a transaction (dat) 62 | - sl - Seal an NFT and lock it from further changes forever 63 | - x extract - move atomical to 0'th output 64 | - y split - 65 | 66 | 其中dft, dmt, nft, ft 都对应不同的payload字段,x y 分别用来转移和拆分atomicals asset。 67 | mod, evt, dat, sl 四个命令目前还没catch,不清楚对应的payload。以上payload字段目前是不完备的,因为我还没有catch全部的payload字段,后续会慢慢完善。 -------------------------------------------------------------------------------- /doc/3.dft.md: -------------------------------------------------------------------------------- 1 | 2 | ## dft - Deploy distributed mint fungible token starting point 3 | ``` 4 | type PayLoad struct { 5 | // ImagePng map[string]string `cbor:"image.png"` // 6 | Args *Args `cbor:"args"` 7 | Meta *Meta `cbor:"meta"` 8 | } 9 | type Args struct { 10 | Nonce int64 `cbor:"nonce"` 11 | Time int64 `cbor:"time"` 12 | Bitworkc string `cbor:"bitworkc"` 13 | MintAmount float64 `cbor:"mint_amount"` 14 | MintHeight int64 `cbor:"mint_height"` 15 | MaxMints int64 `cbor:"max_mints"` 16 | MintBitworkc string `cbor:"mint_bitworkc"` 17 | RequestTicker string `cbor:"request_ticker"` 18 | } 19 | type Meta struct { 20 | Name string `cbor:"name"` 21 | Description string `cbor:"description"` 22 | Legal *Legal `cbor:"legal"` 23 | } 24 | type Legal struct { 25 | Terms string `cbor:"terms"` 26 | } 27 | ``` 28 | 29 | ## indexer 检查条件 30 | - 合法的tickerName IsValidTicker 31 | - 该ticker没有被占用 32 | - 当交易所在提交BlockHeight=ATOMICALS_ACTIVATION_HEIGHT_DENSITY(828128)时,MaxMints需<=DFT_MINT_MAX_MAX_COUNT_DENSITY(21000000) 34 | - 若Bitworkc字段存在,必须通过检查 IsProofOfWorkPrefixMatch 35 | -------------------------------------------------------------------------------- /doc/4.dmt.md: -------------------------------------------------------------------------------- 1 | - dmt - Mint tokens of distributed mint type (dft) 2 | 3 | ``` 4 | type PayLoad struct { 5 | Args *struct { 6 | Nonce int64 `cbor:"nonce"` 7 | Time int64 `cbor:"time"` 8 | Bitworkc string `cbor:"bitworkc"` 9 | MintTicker string `cbor:"mint_ticker"` // mint ft name 10 | } `cbor:"args"` 11 | } 12 | ``` 13 | 14 | ## indexer 检查条件 15 | - 合法的tickerName IsValidTicker 16 | - ticker存在 17 | - tx.Vout[0].Value<=ft.MintAmount 18 | - tx.Vout[0].Value+ft.MintedAmount<=ft.MaxMints 19 | - ft.MintHeight<当前高度 20 | - 若Bitworkc字段存在,必须通过检查 IsProofOfWorkPrefixMatch -------------------------------------------------------------------------------- /doc/5.nft.md: -------------------------------------------------------------------------------- 1 | - nft - Mint non-fungible token 2 | ``` 3 | type PayLoadNftRealm struct { 4 | Args *struct { 5 | Nonce int64 `cbor:"nonce"` 6 | Time int64 `cbor:"time"` 7 | Bitworkc string `cbor:"bitworkc"` 8 | RequestRealm string `cbor:"request_realm"` 9 | RequestDmitem string `cbor:"request_dmitem"` 10 | RequestContainer string `cbor:"request_container"` 11 | 12 | // for SubRealm 13 | RequestSubRealm string `cbor:"request_subrealm"` 14 | ClaimType string `cbor:"claim_type"` // enum: "direct" "rule" 15 | ParentRealm string `cbor:"parent_realm"` // parentRealm atomicalsID 16 | } `cbor:"args"` 17 | } 18 | ``` 19 | 20 | ## indexer 检查条件 21 | - for Realm 22 | - 合法的RealmName IsValidRealm 23 | - 该RealmName没有被占用 24 | - 若Bitworkc字段存在,必须通过检查 IsProofOfWorkPrefixMatch 25 | 26 | - for SubRealm 27 | - 合法的SubRealmName IsValidSubRealm 28 | - parentRealmName 存在 29 | - subRealm没有被占用 30 | - 若Bitworkc字段存在,必须通过检查 IsProofOfWorkPrefixMatch 31 | 32 | - for Container 33 | - 合法的ContainerName IsValidContainer 34 | - 该ContainerName没有被占用 35 | - 若Bitworkc字段存在,必须通过检查 IsProofOfWorkPrefixMatch 36 | 37 | -------------------------------------------------------------------------------- /doc/6.ft.md: -------------------------------------------------------------------------------- 1 | - ft - Mint fungible token with direct fixed supply 2 | ``` 3 | type Args struct { 4 | Nonce int64 `cbor:"nonce"` 5 | Time int64 `cbor:"time"` 6 | Bitworkc string `cbor:"bitworkc"` 7 | 8 | MintAmount float64 `cbor:"mint_amount"` 9 | MintHeight int64 `cbor:"mint_height"` 10 | MaxMints int64 `cbor:"max_mints"` 11 | MintBitworkc string `cbor:"mint_bitworkc"` 12 | RequestTicker string `cbor:"request_ticker"` 13 | } 14 | ``` -------------------------------------------------------------------------------- /doc/pic/atomicals-go-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicals-community/atomicals-go/8e8ceafbad5d89980b1c7cc015bc274eaa1b642e/doc/pic/atomicals-go-framework.png -------------------------------------------------------------------------------- /doc/proposal/split.md: -------------------------------------------------------------------------------- 1 | ### A solution to split atomicals ft 2 | 3 | We need a solution to split Atomicals FT, especially non-integer splitting. 4 | 5 | ### Current Protocol 6 | In the current protocol, each FT is paired with one Satoshi. During transfers, UTXOs that are not fully colored result in the corresponding FT being burned. 7 | 8 | For example, Alice initiates a transfer Tx1: 9 | ``` 10 | Vin[0]: 1000 Satoshis (colored with 1000 Atoms) 11 | Vin[1]: 500 Satoshis 12 | The recipients of the transfer include: 13 | Vout[0]: 800 Satoshis (colored with 800 Atoms) to Bob 14 | Vout[1]: 700 Satoshis to Tom 15 | ``` 16 | According to the current Atomicals protocol, UTXOs that cannot be fully colored correspond to illegal FTs. In the above transaction, 200 Atoms are burned. 17 | 18 | ### Splitting Solution - Supporting Partially Colored UTXOs 19 | In practice, the storage space occupied by the indexer remains the same regardless of whether the UTXO is fully colored or not. The structure of colored UTXOs is defined in atomicals/DB/postsql/atomicalsUTXOFt.go, where the indexer needs to store the ftAmount corresponding to the UTXO, which currently equals the amount of Satoshis. The indexer can record the coloring result of Vout[1] without burned these 200 Atoms. 20 | 21 | If the Atomicals protocol recognizes partially colored UTXOs, the result of the above transaction will be: 22 | ``` 23 | Vout[0]: 800 Satoshis (colored with 800 Atoms) to Bob 24 | Vout[1]: 700 Satoshis (colored with 200 Atoms) to Tom 25 | ``` 26 | 27 | ### Transfer of Partially Colored UTXOs 28 | Tom initiates a new transaction Tx2: 29 | ``` 30 | Vin[0]: 700 Satoshis (colored with 200 Atoms) 31 | The recipients of the transfer include: 32 | Vout[0]: 500 Satoshis (colored with 200*5/7 Atoms) to Jerry 33 | Vout[1]: 200 Satoshis (colored with 200*2/7 Atoms) to Tom 34 | ``` 35 | In this transaction, Jerry and Tom will receive a non-integer number of Atoms, with the decimal part depending on the precision specified by the protocol. If we specify support for 10 decimal places, Jerry will receive 142.8571428571 Atoms, and Tom will receive 57.1428571428. Values beyond the 10th decimal place will be destroyed. 36 | -------------------------------------------------------------------------------- /doc/proposal/swap.md: -------------------------------------------------------------------------------- 1 | ### A solution to swap atomicals ft based on btc chain 2 | 3 | #### creat pool 4 | ``` 5 | // operation and args in witness 6 | "cpool" 7 | type Args struct { 8 | LiquidityATickerName string 9 | LiquidityAAmount int64 10 | LiquidityBTickerName string 11 | LiquidityBAmount int64 12 | LpAmount int64 13 | FeeRate float 14 | } 15 | ``` 16 | Craft a Bitcoin transaction containing a "cpool" operation with accompanying arguments. Subsequently, we'll establish a liquidity pool. Atomicals indexer will then generate a structure containing essential pool information. 17 | ``` 18 | // pool data updated by indexer 19 | type PoolInfo struct { 20 | AtomicalsID string 21 | LiquidityATickerName string 22 | LiquidityAAmount float64 23 | LiquidityBTickerName string 24 | LiquidityBAmount float64 25 | LpAmount float64 26 | FeeRate float 27 | } 28 | ``` 29 | #### add liquidity 30 | ``` 31 | "al" 32 | type Args struct { 33 | PoolAtomicalsID string 34 | } 35 | ``` 36 | When the indexer retrieves an "al" operation, it will examine the transaction inputs (Vins), remove the corresponding UTXOFtInfo entries from the Atomicals database(atomicalsUTXOFt.go), and update the PoolInfo. Additionally, it will designate the transaction outputs (Vouts) as pool tokens by coloring them. 37 | ``` 38 | type PoolToken struct { 39 | AtomicalsID string 40 | UserPk string 41 | PoolAtomicalsID string // pool's AtomicalsID 42 | LpAmount float64 43 | } 44 | ``` 45 | #### remove liquidity 46 | ``` 47 | "rl" 48 | type Args struct { 49 | PoolAtomicalsID string 50 | } 51 | ``` 52 | When the indexer retrieves an "rl" operation, it will examine the transaction inputs (Vins), remove the corresponding PoolToken entries, update the PoolInfo accordingly, and designate the transaction outputs (Vouts) as LiquidityA and LiquidityB by coloring them. 53 | 54 | #### swap 55 | ``` 56 | "swap" 57 | type Args struct { 58 | 59 | PoolAtomicalsID string 60 | } 61 | ``` 62 | When the indexer encounters a "swap" operation, it verifies the transaction inputs (Vins), removes the corresponding UTXOFtInfo entries, updates the PoolInfo accordingly, and designates the transaction outputs (Vouts) as LiquidityA or LiquidityB by coloring them. 63 | 64 | #### some issue 65 | There is an issue with the current system. When a user sends a swap transaction, they prepare specific outputs (Vouts), but the PoolInfo is constantly updated. According to the current rule, when someone swaps 10 units of FtA for 20 units of FtB, if their output value exceeds 20, all of their FtB will be burned. Conversely, if their output value is less than 20, only the excess FtB above 20 will be burned. 66 | 67 | A viable solution is to prioritize UTXOs that haven't been fully colored (i.e., where the value exceeds the amount of tokens). This approach enables the implementation of non-integer splitting of Atomicals Ff. And implementing this approach is straightforward and requires minimal effort. 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/atomicals-go 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/bits-and-blooms/bloom/v3 v3.7.0 7 | github.com/btcsuite/btcd v0.24.0 8 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 9 | github.com/fxamacker/cbor/v2 v2.6.0 10 | github.com/go-echarts/go-echarts/v2 v2.4.0 11 | github.com/go-echarts/snapshot-chromedp v0.0.4 12 | github.com/shopspring/decimal v1.4.0 13 | github.com/stretchr/testify v1.9.0 14 | github.com/zeromicro/go-zero v1.6.6 15 | go.uber.org/zap v1.27.0 16 | gorm.io/driver/postgres v1.5.7 17 | gorm.io/gorm v1.25.7 18 | ) 19 | 20 | require ( 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/bits-and-blooms/bitset v1.12.0 // indirect 23 | github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect 24 | github.com/btcsuite/btcd/btcutil v1.1.5 // indirect 25 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 26 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 27 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 28 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 29 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 30 | github.com/chromedp/cdproto v0.0.0-20240721024200-dac8efcb39ce // indirect 31 | github.com/chromedp/chromedp v0.9.5 // indirect 32 | github.com/chromedp/sysutil v1.0.0 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect 35 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 36 | github.com/fatih/color v1.17.0 // indirect 37 | github.com/go-logr/logr v1.3.0 // indirect 38 | github.com/go-logr/stdr v1.2.2 // indirect 39 | github.com/gobwas/httphead v0.1.0 // indirect 40 | github.com/gobwas/pool v0.2.1 // indirect 41 | github.com/gobwas/ws v1.4.0 // indirect 42 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 43 | github.com/google/uuid v1.6.0 // indirect 44 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect 45 | github.com/icza/mjpeg v0.0.0-20230330134156-38318e5ab8f4 // indirect 46 | github.com/jackc/pgpassfile v1.0.0 // indirect 47 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 48 | github.com/jackc/pgx/v5 v5.5.5 // indirect 49 | github.com/jackc/puddle/v2 v2.2.1 // indirect 50 | github.com/jinzhu/inflection v1.0.0 // indirect 51 | github.com/jinzhu/now v1.1.5 // indirect 52 | github.com/josharian/intern v1.0.0 // indirect 53 | github.com/mailru/easyjson v0.7.7 // indirect 54 | github.com/mattn/go-colorable v0.1.13 // indirect 55 | github.com/mattn/go-isatty v0.0.20 // indirect 56 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 57 | github.com/openzipkin/zipkin-go v0.4.2 // indirect 58 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 59 | github.com/pmezard/go-difflib v1.0.0 // indirect 60 | github.com/prometheus/client_golang v1.18.0 // indirect 61 | github.com/prometheus/client_model v0.5.0 // indirect 62 | github.com/prometheus/common v0.45.0 // indirect 63 | github.com/prometheus/procfs v0.12.0 // indirect 64 | github.com/spaolacci/murmur3 v1.1.0 // indirect 65 | github.com/x448/float16 v0.8.4 // indirect 66 | go.opentelemetry.io/otel v1.19.0 // indirect 67 | go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect 68 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect 69 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect 70 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect 71 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 // indirect 72 | go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect 73 | go.opentelemetry.io/otel/metric v1.19.0 // indirect 74 | go.opentelemetry.io/otel/sdk v1.19.0 // indirect 75 | go.opentelemetry.io/otel/trace v1.19.0 // indirect 76 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 77 | go.uber.org/automaxprocs v1.5.3 // indirect 78 | go.uber.org/multierr v1.10.0 // indirect 79 | golang.org/x/crypto v0.24.0 // indirect 80 | golang.org/x/net v0.26.0 // indirect 81 | golang.org/x/sync v0.7.0 // indirect 82 | golang.org/x/sys v0.22.0 // indirect 83 | golang.org/x/text v0.16.0 // indirect 84 | google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect 85 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect 86 | google.golang.org/grpc v1.64.0 // indirect 87 | google.golang.org/protobuf v1.34.2 // indirect 88 | gopkg.in/yaml.v2 v2.4.0 // indirect 89 | gopkg.in/yaml.v3 v3.0.1 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /pkg/btcsync/account.go: -------------------------------------------------------------------------------- 1 | package btcsync 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | ) 7 | 8 | func (m *BtcSync) GetPublicKeyFromAddress(address string) (string, error) { 9 | // Get address info 10 | addrInfo, err := m.GetAddressInfo(address) 11 | if err != nil { 12 | return "", fmt.Errorf("error getting address info: %v", err) 13 | } 14 | // Decode public key 15 | pubKeyBytes, err := hex.DecodeString(addrInfo.ScriptPubKey) 16 | if err != nil { 17 | return "", fmt.Errorf("error decoding public key: %v", err) 18 | } 19 | return hex.EncodeToString(pubKeyBytes), nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/btcsync/block.go: -------------------------------------------------------------------------------- 1 | package btcsync 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/btcsuite/btcd/btcjson" 7 | "github.com/btcsuite/btcd/chaincfg/chainhash" 8 | ) 9 | 10 | func (m *BtcSync) GetBlockByHeightSync(blockHeight int64) (*btcjson.GetBlockVerboseTxResult, error) { 11 | for height := blockHeight; height < blockHeight+int64(BlockCacheNum); height++ { 12 | if m.CurrentHeight < height { 13 | m.blockHeightChannel <- height 14 | m.CurrentHeight = height 15 | } 16 | } 17 | return m.BlockByHeight(blockHeight), nil 18 | } 19 | 20 | func (m *BtcSync) BlockByHeight(blockHeight int64) *btcjson.GetBlockVerboseTxResult { 21 | var b *btcjson.GetBlockVerboseTxResult 22 | for { 23 | block, ok := m.blockCache.Load(blockHeight) 24 | if ok { 25 | b, _ = block.(*btcjson.GetBlockVerboseTxResult) 26 | m.blockCache.Delete(blockHeight - BlockCacheNum) 27 | break 28 | } 29 | time.Sleep(1 * time.Second) 30 | } 31 | return b 32 | } 33 | 34 | func (m *BtcSync) FetchBlocks() error { 35 | for height := range m.blockHeightChannel { 36 | // set block cache 37 | for { 38 | block, err := m.GetBlockByHeight(height) 39 | if err == nil { 40 | m.blockCache.Store(height, block) 41 | break 42 | } 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | func (m *BtcSync) GetBlockByHeight(height int64) (*btcjson.GetBlockVerboseTxResult, error) { 49 | blockHash, err := m.GetBlockHash(height) 50 | if err != nil { 51 | return nil, err 52 | } 53 | block, err := m.GetBlockVerboseTx(blockHash) 54 | if err != nil { 55 | return nil, err 56 | } 57 | m.CurrentHeight = height 58 | return block, nil 59 | } 60 | 61 | func (m *BtcSync) GetBlockCount() (int64, error) { 62 | return m.Client.GetBlockCount() 63 | } 64 | 65 | func (m *BtcSync) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { 66 | return m.Client.GetBlockHash(blockHeight) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/btcsync/sync.go: -------------------------------------------------------------------------------- 1 | package btcsync 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/btcsuite/btcd/rpcclient" 7 | ) 8 | 9 | const BlockCacheNum = 3 10 | 11 | type BtcSync struct { 12 | *rpcclient.Client 13 | CurrentHeight int64 14 | blockHeightChannel chan int64 15 | blockCache sync.Map 16 | } 17 | 18 | func NewBtcSync(rpcURL, rpcUser, rpcPassword string) (*BtcSync, error) { 19 | client, err := rpcclient.New(&rpcclient.ConnConfig{ 20 | HTTPPostMode: true, 21 | DisableTLS: true, 22 | Host: rpcURL, 23 | User: rpcUser, 24 | Pass: rpcPassword, 25 | }, nil) 26 | if err != nil { 27 | return nil, err 28 | } 29 | m := &BtcSync{ 30 | Client: client, 31 | blockHeightChannel: make(chan int64, BlockCacheNum), 32 | } 33 | go m.FetchBlocks() 34 | return m, nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/btcsync/tx.go: -------------------------------------------------------------------------------- 1 | package btcsync 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/btcjson" 5 | "github.com/btcsuite/btcd/chaincfg/chainhash" 6 | ) 7 | 8 | func (m *BtcSync) GetTxHeightByTxID(txID string) (int64, error) { 9 | t, err := m.GetTransaction(txID) 10 | if err != nil { 11 | return -1, err 12 | } 13 | blockHash, err := chainhash.NewHashFromStr(t.BlockHash) 14 | if err != nil { 15 | return -1, err 16 | } 17 | blockInfo, err := m.GetBlockVerbose(blockHash) 18 | if err != nil { 19 | return -1, err 20 | } 21 | return blockInfo.Height, nil 22 | } 23 | 24 | func (m *BtcSync) GetTxByTxID(txID string) (*btcjson.TxRawResult, int64, error) { 25 | t, err := m.GetTransaction(txID) 26 | if err != nil { 27 | return nil, -1, err 28 | } 29 | if t.BlockHash == "" { 30 | return nil, -1, nil 31 | } 32 | blockHash, err := chainhash.NewHashFromStr(t.BlockHash) 33 | if err != nil { 34 | return nil, -1, err 35 | } 36 | blockInfo, err := m.GetBlockVerboseTx(blockHash) 37 | if err != nil { 38 | return nil, -1, err 39 | } 40 | return t, blockInfo.Height, nil 41 | } 42 | 43 | func (m *BtcSync) GetTransaction(txID string) (*btcjson.TxRawResult, error) { 44 | hash, err := chainhash.NewHashFromStr(txID) 45 | if err != nil { 46 | return nil, err 47 | } 48 | rawTx, err := m.GetRawTransactionVerbose(hash) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return rawTx, nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | type Config struct { 10 | BtcRpcURL string `json:"btc_rpc_url"` 11 | BtcRpcUser string `json:"btc_rpc_user"` 12 | BtcRpcPassword string `json:"btc_rpc_password"` 13 | SqlDNS string `json:"sql_dns"` 14 | AtomicalsStartHeight int64 `json:"atomicals_start_height"` 15 | } 16 | 17 | func ReadJSONFromJSFile(filePath string) (*Config, error) { 18 | // Open the .js file 19 | file, err := os.Open(filePath) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer file.Close() 24 | 25 | // Read the file content 26 | byteValue, err := ioutil.ReadAll(file) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | // Unmarshal the JSON data into the struct 32 | var myData Config 33 | err = json.Unmarshal(byteValue, &myData) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return &myData, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/errors/api.go: -------------------------------------------------------------------------------- 1 | // error: Custom error type 2 | // using method: 3 | // err := errors.New(10000, "Example error msg") 4 | package errors 5 | 6 | type Error interface { 7 | Error() string 8 | // Msg() string 9 | // Code() int64 10 | // RefineError(err ...interface{}) *error 11 | } 12 | 13 | func New(code int64, msg string) Error { 14 | return new(code, msg) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/errors/const.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | var ( 4 | // error in package 5 | ErrNotExistBlock = New(10001, "ErrNotExistBlock") 6 | 7 | // error in witness package 8 | ErrInvalidWitnessScriptLength = New(10001, "ErrInvalidWitnessScriptLength") 9 | ErrInvalidWitnessScriptPkFlag = New(10002, "ErrInvalidWitnessScriptPkFlag") 10 | ErrInvalidPayLoad = New(10003, "ErrInvalidPayLoad") 11 | ErrOptionNotFound = New(10004, "ErrOptionNotFound") 12 | ErrInvalidAtomicalsData = New(10005, "ErrInvalidAtomicalsData") 13 | 14 | // error in atomicals package 15 | ErrInvalidCommitHeight = New(20000, "ErrInvalidCommitHeight") 16 | ErrInvalidLocation = New(20000, "ErrInvalidLocation") 17 | ErrInvalidCommitVoutIndex = New(20000, "ErrInvalidCommitVoutIndex") 18 | ErrInvalidFtCurrentHeight = New(20001, "ErrInvalidFtCurrentHeight") 19 | ErrInvalidTicker = New(20002, "ErrInvalidTicker") 20 | ErrTickerHasExist = New(20003, "ErrTickerHasExist") 21 | ErrTickerNotExist = New(20003, "ErrTickerNotExist") 22 | ErrInvalidBitWork = New(20004, "ErrInvalidBitWork") 23 | ErrInvalidRealm = New(20005, "ErrInvalidRealm") 24 | ErrRealmHasExist = New(20005, "ErrRealmHasExist") 25 | ErrInvalidContainer = New(20006, "ErrInvalidContainer") 26 | ErrInvalidContainerDmitem = New(20006, "ErrInvalidContainerDmitem") 27 | ErrContainerHasExist = New(20005, "ErrContainerHasExist") 28 | ErrContainerNotExist = New(20005, "ErrContainerNotExist") 29 | ErrParentRealmNotExist = New(20005, "ErrParentRealmNotExist") 30 | ErrSubRealmHasExist = New(20005, "ErrSubRealmHasExist") 31 | ErrNotDeployFt = New(20007, "ErrNotDeployFt") 32 | ErrInvalidMintAmount = New(20008, "ErrInvalidMintAmount") 33 | ErrInvalidMintHeight = New(20009, "ErrInvalidMintHeight") 34 | ErrInvalidMaxMints = New(20010, "ErrInvalidMaxMints") 35 | ErrCannotBeImmutable = New(20010, "ErrCannotBeImmutable") 36 | ErrInvalidVinIndex = New(20010, "ErrInvalidVinIndex") 37 | ErrInvalidClaimType = New(20010, "ErrInvalidClaimType") 38 | ErrInvalidRule = New(20010, "ErrInvalidRule") 39 | ErrInvalidDftMd = New(20010, "ErrInvalidDftMd") 40 | ErrInvalidDftBv = New(20010, "ErrInvalidDftBv") 41 | ErrInvalidDftMintBitwork = New(20010, "ErrInvalidDftMintBitwork") 42 | ErrInvalidDftBci = New(20010, "ErrInvalidDftBci") 43 | ErrInvalidDftBsc = New(20010, "ErrInvalidDftBsc") 44 | ErrInvalidDftBri = New(20010, "ErrInvalidDftBri") 45 | ErrInvalidDftBrs = New(20010, "ErrInvalidDftBrs") 46 | ErrInvalidDftMaxg = New(20010, "ErrInvalidDftMaxg") 47 | ErrNameTypeMintMastHaveBitworkc = New(20010, "ErrNameTypeMintMastHaveBitworkc") 48 | ErrInvalidPerpetualBitwork = New(20010, "ErrInvalidPerpetualBitwork") 49 | ErrInvalidMintedTimes = New(20010, "ErrInvalidMintedTimes") 50 | ErrInvalidBitworkcPrefix = New(20010, "ErrInvalidBitworkcPrefix") 51 | ErrBitworkcNeeded = New(20010, "ErrBitworkcNeeded") 52 | ErrCheckRequest = New(20010, "ErrCheckRequest") 53 | ErrDmintNotStart = New(20010, "ErrDmintNotStart") 54 | ErrInvalidRevealInputIndex = New(20010, "ErrInvalidRevealInputIndex") 55 | ErrInvalidMerkleVerify = New(20010, "ErrInvalidMerkleVerify") 56 | ) 57 | -------------------------------------------------------------------------------- /pkg/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | type error struct { 6 | code int64 7 | message string 8 | } 9 | 10 | func (e *error) Error() string { 11 | return fmt.Sprintf("%d: %s", e.code, e.message) 12 | } 13 | 14 | func (e *error) Msg() string { 15 | return e.message 16 | } 17 | 18 | func (e *error) Code() int64 { 19 | return e.code 20 | } 21 | 22 | func (e *error) RefineError(err ...interface{}) *error { 23 | return new(e.Code(), e.message+", "+fmt.Sprint(err...)) 24 | } 25 | 26 | func new(code int64, msg string) *error { 27 | return &error{ 28 | code: code, 29 | message: msg, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/log/zaplog.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "log" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | var ( 10 | Log = NewZaLop() 11 | ) 12 | 13 | func NewZaLop() *zap.SugaredLogger { 14 | logger, err := zap.NewProduction() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | sugar := logger.Sugar() 19 | defer logger.Sync() 20 | return sugar 21 | } 22 | 23 | func Error(msg string) (*zap.SugaredLogger, string) { 24 | Log.Error("this is an error message") 25 | return Log, msg 26 | } 27 | -------------------------------------------------------------------------------- /pkg/merkle/merkle.go: -------------------------------------------------------------------------------- 1 | package merkle 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | 7 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 8 | ) 9 | 10 | type MerkleNode struct { 11 | Right []byte 12 | Left []byte 13 | } 14 | 15 | // check_validate_proof 16 | func CheckValidateProof(expected_root_hash, target_hash []byte, proof []witness.Proof) bool { 17 | formatted_proof := make([]*MerkleNode, 0) 18 | for _, item := range proof { 19 | d, err := hex.DecodeString(item.D) 20 | if err != nil { 21 | return false 22 | } 23 | if item.P { 24 | formatted_proof = append(formatted_proof, &MerkleNode{ 25 | Right: d, 26 | }) 27 | } else { 28 | formatted_proof = append(formatted_proof, &MerkleNode{ 29 | Left: d, 30 | }) 31 | } 32 | } 33 | return ValidateProof(formatted_proof, target_hash, expected_root_hash) 34 | } 35 | 36 | func ValidateProof(proof []*MerkleNode, computedHash, expectedRootHash []byte) bool { 37 | for _, node := range proof { 38 | if node.Left != nil && node.Right != nil { 39 | return false // Both left and right hashes are provided, which is invalid 40 | } 41 | if node.Left != nil { 42 | computedHash = ComputeParentHash(node.Left, computedHash) 43 | } else { 44 | computedHash = ComputeParentHash(computedHash, node.Right) 45 | } 46 | } 47 | return hex.EncodeToString(computedHash) == hex.EncodeToString(expectedRootHash) 48 | } 49 | 50 | func ComputeParentHash(leftHash, rightHash []byte) []byte { 51 | // Concatenate the left and right hashes and hash the result using SHA-256 52 | combinedHash := append(leftHash, rightHash...) 53 | hash := sha256.New() 54 | hash.Write([]byte(combinedHash)) 55 | return hash.Sum(nil) 56 | } 57 | -------------------------------------------------------------------------------- /repo/api.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "github.com/atomicals-go/repo/postsql" 5 | "gorm.io/driver/postgres" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | //go:generate mockgen -source api.go -destination api_mock.go -package repo 10 | type DB interface { 11 | // location 12 | Location() (*postsql.Location, error) 13 | 14 | // btc 15 | AtomicalsTx(txID string) (*postsql.AtomicalsTx, error) 16 | AtomicalsTxByHeight(height int64) ([]*postsql.AtomicalsTx, error) 17 | AtomicalsTxHeight(txID string) (int64, error) 18 | 19 | // nft read 20 | NftUTXOsByUserPK(UserPK string) ([]*postsql.UTXONftInfo, error) 21 | NftUTXOByAtomicalsID(atomicalsID string) (*postsql.UTXONftInfo, error) 22 | NftUTXOsByLocationID(locationID string) ([]*postsql.UTXONftInfo, error) 23 | NftRealmByName(realmName string) ([]*postsql.UTXONftInfo, error) 24 | NftSubRealmByNameHasExist(parentRealmAtomicalsID, subRealm string) (bool, error) 25 | NftContainerByNameHasExist(containerName string) (bool, error) 26 | ContainerItemByNameHasExist(container, item string) (bool, error) 27 | LatestItemByContainerName(container string) (*postsql.UTXONftInfo, error) 28 | 29 | // ft read 30 | FtUTXOsByUserPK(UserPK string) ([]*postsql.UTXOFtInfo, error) 31 | FtUTXOsByLocationID(locationID string) ([]*postsql.UTXOFtInfo, error) 32 | DistributedFtByName(tickerName string) (*postsql.GlobalDistributedFt, error) 33 | DirectFtByName(tickerName string) (*postsql.GlobalDirectFt, error) 34 | 35 | // mod 36 | ModHistory(atomicalsID string, height int64) ([]*postsql.ModInfo, error) 37 | 38 | UpdateDB(location *postsql.Location, data *AtomicaslData) error 39 | 40 | PostgresDB() *Postgres 41 | } 42 | 43 | func NewSqlDB(sqlDNS string) DB { 44 | DB, err := gorm.Open(postgres.Open(sqlDNS)) 45 | if err != nil { 46 | panic(err) 47 | } 48 | s := &Postgres{ 49 | DB: DB, 50 | } 51 | s.bloomFilter, err = s.BloomFilter() 52 | if err != nil { 53 | panic(err) 54 | } 55 | return s 56 | } 57 | -------------------------------------------------------------------------------- /repo/atomicalsTx.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/atomicals-go/repo/postsql" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func (m *Postgres) Location() (*postsql.Location, error) { 11 | entity := &postsql.Location{} 12 | dbTx := m.Order("id desc").First(&entity) 13 | if dbTx.Error != nil { 14 | return nil, dbTx.Error 15 | } 16 | if dbTx.RowsAffected == 0 { 17 | return nil, gorm.ErrRecordNotFound 18 | } 19 | return entity, nil 20 | } 21 | 22 | func (m *Postgres) AtomicalsTx(txID string) (*postsql.AtomicalsTx, error) { 23 | var entity *postsql.AtomicalsTx 24 | dbTx := m.Where("tx_id = ?", txID).Find(&entity) 25 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 26 | return nil, dbTx.Error 27 | } 28 | if dbTx.RowsAffected == 0 { 29 | return nil, nil 30 | } 31 | return entity, nil 32 | } 33 | 34 | func (m *Postgres) AtomicalsTxHeight(txID string) (int64, error) { 35 | var entity *postsql.AtomicalsTx 36 | dbTx := m.Where("tx_id = ?", txID).Find(&entity) 37 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 38 | return -1, dbTx.Error 39 | } 40 | if dbTx.RowsAffected == 0 { 41 | return -1, gorm.ErrRecordNotFound 42 | } 43 | return entity.BlockHeight, nil 44 | } 45 | 46 | func (m *Postgres) AtomicalsTxByHeight(height int64) ([]*postsql.AtomicalsTx, error) { 47 | var entity []*postsql.AtomicalsTx 48 | dbTx := m.Where("block_height = ?", height).Order("id").Find(&entity) 49 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 50 | return nil, dbTx.Error 51 | } 52 | return entity, nil 53 | } 54 | -------------------------------------------------------------------------------- /repo/bloomFilter.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/atomicals-go/repo/postsql" 7 | "github.com/bits-and-blooms/bloom/v3" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type bloomFilterInfo struct { 12 | filter *bloom.BloomFilter 13 | needUpdate bool 14 | } 15 | 16 | // ft 17 | func (m *Postgres) addFtLocationID(locationID string) { 18 | m.bloomFilter[postsql.FtLocationFilter].filter.Add([]byte(fmt.Sprintf("%v_%v", postsql.TypeDirectFt, locationID))) 19 | m.bloomFilter[postsql.FtLocationFilter].needUpdate = true 20 | } 21 | 22 | func (m *Postgres) testFtLocationID(locationID string) bool { 23 | return m.bloomFilter[postsql.FtLocationFilter].filter.Test([]byte(fmt.Sprintf("%v_%v", postsql.TypeDirectFt, locationID))) 24 | } 25 | 26 | // func (m *Postgres) addDistributedFt(ftName string) { 27 | // m.bloomFilter[postsql.FtFilter].filter.Add([]byte(fmt.Sprintf("%v_%v", postsql.TypeDistributedFt, ftName))) 28 | // m.bloomFilter[postsql.FtFilter].needUpdate = true 29 | // } 30 | 31 | // func (m *Postgres) testDistributedFt(ftName string) bool { 32 | // return m.bloomFilter[postsql.FtFilter].filter.Test([]byte(fmt.Sprintf("%v_%v", postsql.TypeDistributedFt, ftName))) 33 | // } 34 | 35 | // nft 36 | func (m *Postgres) addRealm(realm string) { 37 | m.bloomFilter[postsql.NftFilter].filter.Add([]byte(fmt.Sprintf("%v_%v", postsql.TypeNftRealm, realm))) 38 | m.bloomFilter[postsql.NftFilter].needUpdate = true 39 | } 40 | 41 | func (m *Postgres) testRealm(realm string) bool { 42 | return m.bloomFilter[postsql.NftFilter].filter.Test([]byte(fmt.Sprintf("%v_%v", postsql.TypeNftRealm, realm))) 43 | } 44 | 45 | func (m *Postgres) addContainer(container string) { 46 | m.bloomFilter[postsql.NftFilter].filter.Add([]byte(fmt.Sprintf("%v_%v", postsql.TypeNftContainer, container))) 47 | m.bloomFilter[postsql.NftFilter].needUpdate = true 48 | } 49 | 50 | func (m *Postgres) testContainer(container string) bool { 51 | return m.bloomFilter[postsql.NftFilter].filter.Test([]byte(fmt.Sprintf("%v_%v", postsql.TypeNftContainer, container))) 52 | } 53 | 54 | func (m *Postgres) addNftLocationID(locationID string) { 55 | m.bloomFilter[postsql.NftLocationFilter].filter.Add([]byte(fmt.Sprintf("%v_%v", postsql.TypeDirectFt, locationID))) 56 | m.bloomFilter[postsql.NftLocationFilter].needUpdate = true 57 | } 58 | 59 | func (m *Postgres) testNftLocationID(locationID string) bool { 60 | return m.bloomFilter[postsql.NftLocationFilter].filter.Test([]byte(fmt.Sprintf("%v_%v", postsql.TypeDirectFt, locationID))) 61 | } 62 | 63 | func (m *Postgres) BloomFilter() (map[string]*bloomFilterInfo, error) { 64 | entities := make([]*postsql.BloomFilter, 0) 65 | dbTx := m.Order("id desc").Find(&entities) 66 | if dbTx.Error != nil { 67 | return nil, dbTx.Error 68 | } 69 | if dbTx.RowsAffected == 0 { 70 | return nil, gorm.ErrRecordNotFound 71 | } 72 | 73 | filterMap := make(map[string]*bloomFilterInfo, 0) 74 | for _, v := range entities { 75 | filter := bloom.NewWithEstimates(10000, 0.01) 76 | filter.UnmarshalJSON(v.Data) 77 | filterMap[v.Name] = &bloomFilterInfo{ 78 | filter: filter, 79 | needUpdate: false, 80 | } 81 | } 82 | return filterMap, nil 83 | } 84 | -------------------------------------------------------------------------------- /repo/db.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/atomicals-go/repo/postsql" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type Postgres struct { 11 | *gorm.DB 12 | bloomFilter map[string]*bloomFilterInfo 13 | } 14 | type AtomicaslData struct { 15 | Op string 16 | Description string 17 | Mod *postsql.ModInfo 18 | Dat *postsql.DatInfo 19 | DeleteFts []*postsql.UTXOFtInfo 20 | NewFts []*postsql.UTXOFtInfo 21 | UpdateNfts []*postsql.UTXONftInfo 22 | NewUTXOFtInfo *postsql.UTXOFtInfo 23 | UpdateDistributedFt *postsql.GlobalDistributedFt 24 | NewGlobalDistributedFt *postsql.GlobalDistributedFt 25 | NewGlobalDirectFt *postsql.GlobalDirectFt 26 | NewUTXONftInfo *postsql.UTXONftInfo 27 | DeleteUTXONfts []*postsql.UTXONftInfo 28 | } 29 | 30 | func (m *AtomicaslData) ParseOperation(orgOp string) { 31 | if m == nil { 32 | return 33 | } 34 | 35 | // transfer ft 36 | if len(m.DeleteFts) > 0 { 37 | for _, v := range m.DeleteFts { 38 | m.Description += fmt.Sprintf("delete#ticker:%v,locationID:%v,userPk:%v,amount:%v\n", v.MintTicker, v.LocationID, v.UserPk, v.Amount) 39 | } 40 | m.Op = "transfer" 41 | if orgOp == "x" { 42 | m.Op = "splat" 43 | } else if orgOp == "y" { 44 | m.Op = "split" 45 | } 46 | } 47 | if len(m.NewFts) > 0 { 48 | for _, v := range m.NewFts { 49 | m.Description += fmt.Sprintf("insert#ticker:%v,locationID:%v,userPk:%v,amount:%v\n", v.MintTicker, v.LocationID, v.UserPk, v.Amount) 50 | } 51 | } 52 | 53 | // transfer nft 54 | if len(m.UpdateNfts) > 0 { 55 | m.Op = "transfer" 56 | if orgOp == "x" { 57 | m.Op = "splat" 58 | } else if orgOp == "y" { 59 | m.Op = "split" 60 | } 61 | } 62 | 63 | // mod 64 | if m.Mod != nil { 65 | m.Op = "mod" 66 | } 67 | 68 | if m.Dat != nil { 69 | m.Op = "dat" 70 | } 71 | 72 | // mint ft 73 | if m.NewUTXOFtInfo != nil { 74 | m.Op = "mint-dft" 75 | m.Description += fmt.Sprintf("insert#ticker:%v,locationID:%v,userPk:%v,amount:%v\n", m.NewUTXOFtInfo.MintTicker, m.NewUTXOFtInfo.LocationID, m.NewUTXOFtInfo.UserPk, m.NewUTXOFtInfo.Amount) 76 | } else { 77 | if orgOp == "dmt" { 78 | m.Op = "mint-dft-failed" 79 | } 80 | } 81 | if m.UpdateDistributedFt != nil { 82 | m.Op = "dmt" 83 | } 84 | if m.NewGlobalDistributedFt != nil { 85 | m.Op = "dft" 86 | } 87 | if m.NewGlobalDirectFt != nil { 88 | m.Op = "ft" 89 | } 90 | 91 | // mint nft 92 | if m.NewUTXONftInfo != nil { 93 | if m.NewUTXONftInfo.RealmName != "" { 94 | m.Op = "mint-nft-realm" 95 | } 96 | if m.NewUTXONftInfo.SubRealmName != "" { 97 | m.Op = "mint-nft-subrealm" 98 | } 99 | if m.NewUTXONftInfo.ContainerName != "" { 100 | m.Op = "mint-nft-container" 101 | } 102 | if m.NewUTXONftInfo.Dmitem != "" { 103 | m.Op = "mint-nft" 104 | } 105 | } 106 | } 107 | 108 | func (m *Postgres) UpdateDB(location *postsql.Location, data *AtomicaslData) error { 109 | if data == nil { 110 | return nil 111 | } 112 | 113 | if !((data.Op != "") || (location.BlockHeight%10 == 0 && location.TxIndex == 0)) { 114 | return nil 115 | } 116 | 117 | err := m.Transaction(func(tx *gorm.DB) error { 118 | // mod 119 | if data.Mod != nil { 120 | dbErr := tx.Save(data.Mod) 121 | if dbErr.Error != nil { 122 | return dbErr.Error 123 | } 124 | } 125 | 126 | if data.Dat != nil { 127 | dbErr := tx.Save(data.Dat) 128 | if dbErr.Error != nil { 129 | return dbErr.Error 130 | } 131 | } 132 | 133 | // transfer ft 134 | if len(data.DeleteFts) > 0 { 135 | locationIDs := make([]string, len(data.DeleteFts)) 136 | for i, v := range data.DeleteFts { 137 | locationIDs[i] = v.LocationID 138 | } 139 | if dbErr := tx.Model(&postsql.UTXOFtInfo{}).Unscoped().Where("location_id IN ?", locationIDs).Delete(&postsql.UTXOFtInfo{}).Error; dbErr != nil { 140 | return dbErr 141 | } 142 | } 143 | if len(data.NewFts) > 0 { 144 | for _, v := range data.NewFts { 145 | m.addFtLocationID(v.LocationID) 146 | } 147 | if dbErr := tx.Create(&data.NewFts).Error; dbErr != nil { 148 | return dbErr 149 | } 150 | } 151 | 152 | // transfer nft 153 | if len(data.UpdateNfts) > 0 { 154 | for _, v := range data.UpdateNfts { 155 | m.addNftLocationID(v.LocationID) 156 | } 157 | if dbErr := tx.Save(&data.UpdateNfts).Error; dbErr != nil { 158 | return dbErr 159 | } 160 | } 161 | 162 | // mint ft 163 | if data.NewUTXOFtInfo != nil { 164 | m.addFtLocationID(data.NewUTXOFtInfo.LocationID) 165 | if dbErr := tx.Save(data.NewUTXOFtInfo).Error; dbErr != nil { 166 | return dbErr 167 | } 168 | } 169 | if data.UpdateDistributedFt != nil { 170 | dbErr := tx.Model(postsql.GlobalDistributedFt{}).Where("ticker_name = ?", data.UpdateDistributedFt.TickerName).Updates(map[string]interface{}{"minted_times": data.UpdateDistributedFt.MintedTimes}) 171 | if dbErr.Error != nil { 172 | return dbErr.Error 173 | } 174 | } 175 | if data.NewGlobalDistributedFt != nil { 176 | // m.addDistributedFt(data.NewGlobalDistributedFt.TickerName) 177 | m.addFtLocationID(data.NewGlobalDistributedFt.LocationID) 178 | dbErr := tx.Save(data.NewGlobalDistributedFt) 179 | if dbErr.Error != nil { 180 | return dbErr.Error 181 | } 182 | } 183 | if data.NewGlobalDirectFt != nil { 184 | m.addFtLocationID(data.NewGlobalDirectFt.LocationID) 185 | dbErr := tx.Save(data.NewGlobalDirectFt) 186 | if dbErr.Error != nil { 187 | return dbErr.Error 188 | } 189 | } 190 | 191 | // mint nft 192 | if data.NewUTXONftInfo != nil { 193 | if data.NewUTXONftInfo.RealmName != "" { 194 | m.addRealm(data.NewUTXONftInfo.RealmName) 195 | } else if data.NewUTXONftInfo.ContainerName != "" { 196 | m.addContainer(data.NewUTXONftInfo.ContainerName) 197 | } 198 | m.addNftLocationID(data.NewUTXONftInfo.LocationID) 199 | dbErr := tx.Save(data.NewUTXONftInfo) 200 | if dbErr.Error != nil { 201 | return dbErr.Error 202 | } 203 | } 204 | 205 | if len(data.DeleteUTXONfts) != 0 { 206 | dbErr := tx.Delete(&data.DeleteUTXONfts) 207 | if dbErr.Error != nil { 208 | return dbErr.Error 209 | } 210 | } 211 | 212 | // update bloom filter 213 | for name, v := range m.bloomFilter { 214 | if v.needUpdate { 215 | data, err := v.filter.MarshalJSON() 216 | if err != nil { 217 | return err 218 | } 219 | dbErr := tx.Model(postsql.BloomFilter{}).Where("name = ?", name).Update("data", data) 220 | if dbErr.Error != nil { 221 | return dbErr.Error 222 | } 223 | v.needUpdate = false 224 | } 225 | } 226 | 227 | // insert btc tx record 228 | if data.Op != "" { 229 | dbErr := tx.Save(&postsql.AtomicalsTx{ 230 | BlockHeight: location.BlockHeight, 231 | TxIndex: location.TxIndex, 232 | TxID: location.Txid, 233 | Operation: data.Op, 234 | Description: data.Description, 235 | }) 236 | if dbErr.Error != nil { 237 | return dbErr.Error 238 | } 239 | } 240 | 241 | // update location 242 | dbErr := tx.Model(postsql.Location{}).Where("key = ?", postsql.LocationKey).Save(location) 243 | if dbErr.Error != nil { 244 | return dbErr.Error 245 | } 246 | 247 | // we don't need save all height-txid in db, delete atomicals tx until 248 | // if currentTxIndex == 0 { 249 | // dbErr := tx.Model(postsql.AtomicalsTx{}).Unscoped().Where("block_height = ? and operation = ?", currentHeight-utils.MINT_GENERAL_COMMIT_REVEAL_DELAY_BLOCKS, "").Delete(&postsql.AtomicalsTx{}) 250 | // if dbErr.Error != nil { 251 | // return dbErr.Error 252 | // } 253 | // } 254 | return nil 255 | }) 256 | return err 257 | } 258 | 259 | func (m *Postgres) PostgresDB() *Postgres { 260 | return m 261 | } 262 | -------------------------------------------------------------------------------- /repo/ftRead.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/atomicals-go/repo/postsql" 7 | ) 8 | 9 | func (m *Postgres) FtUTXOsByUserPK(UserPK string) ([]*postsql.UTXOFtInfo, error) { 10 | var entity []*postsql.UTXOFtInfo 11 | dbTx := m.Model(postsql.UTXOFtInfo{}).Where("user_pk = ?", UserPK).Find(&entity) 12 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 13 | return nil, dbTx.Error 14 | } 15 | // if dbTx.RowsAffected == 0 { 16 | // return nil, nil 17 | // } 18 | return entity, nil 19 | } 20 | 21 | func (m *Postgres) FtUTXOsByLocationID(locationID string) ([]*postsql.UTXOFtInfo, error) { 22 | if !m.testFtLocationID(locationID) { 23 | return nil, nil 24 | } 25 | var entity []*postsql.UTXOFtInfo 26 | dbTx := m.Model(postsql.UTXOFtInfo{}).Where("location_id = ?", locationID).Find(&entity) 27 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 28 | return nil, dbTx.Error 29 | } 30 | return entity, nil 31 | } 32 | 33 | func (m *Postgres) DistributedFtByName(tickerName string) (*postsql.GlobalDistributedFt, error) { 34 | // if !m.testDistributedFt(tickerName) { 35 | // return nil, nil 36 | // } 37 | var entity *postsql.GlobalDistributedFt 38 | dbTx := m.Model(postsql.GlobalDistributedFt{}).Where("ticker_name = ?", tickerName).Find(&entity) 39 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 40 | return nil, dbTx.Error 41 | } 42 | if dbTx.RowsAffected == 0 { 43 | return nil, nil 44 | } 45 | return entity, nil 46 | } 47 | 48 | func (m *Postgres) DirectFtByName(tickerName string) (*postsql.GlobalDirectFt, error) { 49 | var entity *postsql.GlobalDirectFt 50 | dbTx := m.Model(postsql.GlobalDirectFt{}).Where("ticker_name = ?", tickerName).Find(&entity) 51 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 52 | return nil, dbTx.Error 53 | } 54 | if dbTx.RowsAffected == 0 { 55 | return nil, nil 56 | } 57 | return entity, nil 58 | } 59 | 60 | func (m *Postgres) FtUTXOsByID(offset, limit int) ([]*postsql.UTXOFtInfo, error) { 61 | var entity []*postsql.UTXOFtInfo 62 | dbTx := m.Model(postsql.UTXOFtInfo{}).Order("id").Offset(offset).Limit(limit).Find(&entity) 63 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 64 | return nil, dbTx.Error 65 | } 66 | return entity, nil 67 | } 68 | 69 | func (m *Postgres) DistributedFtByID(offset, limit int) ([]*postsql.GlobalDistributedFt, error) { 70 | var entity []*postsql.GlobalDistributedFt 71 | dbTx := m.Model(postsql.GlobalDistributedFt{}).Order("id").Offset(offset).Limit(limit).Find(&entity) 72 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 73 | return nil, dbTx.Error 74 | } 75 | return entity, nil 76 | } 77 | -------------------------------------------------------------------------------- /repo/mod.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/atomicals-go/repo/postsql" 7 | ) 8 | 9 | func (m *Postgres) ModHistory(atomicalsID string, height int64) ([]*postsql.ModInfo, error) { 10 | var entities []*postsql.ModInfo 11 | dbTx := m.Where("atomicals_id = ? and height >= ?", atomicalsID, height).Order("id").Find(&entities) 12 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 13 | return nil, dbTx.Error 14 | } 15 | if dbTx.RowsAffected == 0 { 16 | return nil, nil 17 | } 18 | return entities, nil 19 | } 20 | -------------------------------------------------------------------------------- /repo/nftRead.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/atomicals-go/repo/postsql" 7 | ) 8 | 9 | func (m *Postgres) NftUTXOsByUserPK(UserPK string) ([]*postsql.UTXONftInfo, error) { 10 | var entity []*postsql.UTXONftInfo 11 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("user_pk = ?", UserPK).Find(&entity) 12 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 13 | return nil, dbTx.Error 14 | } 15 | return entity, nil 16 | } 17 | 18 | func (m *Postgres) NftUTXOByAtomicalsID(atomicalsID string) (*postsql.UTXONftInfo, error) { 19 | var entity *postsql.UTXONftInfo 20 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("atomicals_id = ?", atomicalsID).Find(&entity) 21 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 22 | return nil, dbTx.Error 23 | } 24 | return entity, nil 25 | } 26 | 27 | func (m *Postgres) NftUTXOsByLocationID(locationID string) ([]*postsql.UTXONftInfo, error) { 28 | if !m.testNftLocationID(locationID) { 29 | return nil, nil 30 | } 31 | var entity []*postsql.UTXONftInfo 32 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("location_id = ?", locationID).Find(&entity) 33 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 34 | return nil, dbTx.Error 35 | } 36 | return entity, nil 37 | } 38 | 39 | func (m *Postgres) NftRealmByName(realmName string) ([]*postsql.UTXONftInfo, error) { 40 | if !m.testRealm(realmName) { 41 | return nil, nil 42 | } 43 | var entities []*postsql.UTXONftInfo 44 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("realm_name = ?", realmName).First(&entities) 45 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 46 | return nil, dbTx.Error 47 | } 48 | 49 | if len(entities) == 0 { 50 | return nil, nil 51 | } 52 | return entities, nil 53 | } 54 | 55 | func (m *Postgres) NftSubRealmByNameHasExist(parentRealmAtomicalsID, subRealm string) (bool, error) { 56 | var entities []*postsql.UTXONftInfo 57 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("parent_realm_atomicals_id = ? and sub_realm_name = ?", parentRealmAtomicalsID, subRealm).First(&entities) 58 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 59 | return false, dbTx.Error 60 | } 61 | if dbTx.RowsAffected == 0 { 62 | return false, nil 63 | } 64 | return true, nil 65 | } 66 | 67 | func (m *Postgres) NftContainerByNameHasExist(containerName string) (bool, error) { 68 | if !m.testContainer(containerName) { 69 | return false, nil 70 | } 71 | var entities []*postsql.UTXONftInfo 72 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("container_name = ?", containerName).First(&entities) 73 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 74 | return false, dbTx.Error 75 | } 76 | if dbTx.RowsAffected == 0 { 77 | return false, nil 78 | } 79 | return true, nil 80 | } 81 | 82 | func (m *Postgres) ContainerItemByNameHasExist(containerName, itemID string) (bool, error) { 83 | var entities []*postsql.UTXONftInfo 84 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("container_name = ? and dmitem = ?", containerName, itemID).Find(&entities) 85 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 86 | return false, dbTx.Error 87 | } 88 | if dbTx.RowsAffected == 0 { 89 | return false, nil 90 | } 91 | return true, nil 92 | } 93 | 94 | func (m *Postgres) LatestItemByContainerName(containerName string) (*postsql.UTXONftInfo, error) { 95 | var entity *postsql.UTXONftInfo 96 | dbTx := m.Model(postsql.UTXONftInfo{}).Where("container_name = ?", containerName).Order("id DESC").Find(&entity) 97 | if dbTx.Error != nil && !strings.Contains(dbTx.Error.Error(), "record not found") { 98 | return nil, dbTx.Error 99 | } 100 | if dbTx.RowsAffected == 0 { 101 | return nil, nil 102 | } 103 | return nil, nil 104 | } 105 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsDat.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const DatTableName = "atomicals_dat" 9 | 10 | type DatInfo struct { 11 | gorm.Model 12 | Height int64 13 | AtomicalsID string `gorm:"index"` // txID _ VOUT_EXPECT_OUTPUT_INDEX when be minted 14 | LocationID string `gorm:"index"` // txID_voutIndex updated after being transfered 15 | Dat string 16 | } 17 | 18 | func (*DatInfo) TableName() string { 19 | return DatTableName 20 | } 21 | 22 | func (*DatInfo) Init(db *gorm.DB) { 23 | var err error 24 | dmodel := newDefaultModel(DatTableName, db) 25 | err = dmodel.DropTable() 26 | assert.Nil(nil, err) 27 | err = dmodel.CreateTable(&DatInfo{}) 28 | assert.Nil(nil, err) 29 | } 30 | 31 | func (*DatInfo) AutoMigrate(db *gorm.DB) { 32 | err := db.AutoMigrate(DatInfo{}) 33 | assert.Nil(nil, err) 34 | } 35 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsGlobalDirectFt.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const globalDirectFtTableName = "atomicals_global_direct_ft" 9 | 10 | type GlobalDirectFt struct { 11 | gorm.Model 12 | UserPk string 13 | AtomicalsID string `gorm:"index"` // (txID,VOUT_EXPECT_OUTPUT_INDEX) init when be minted 14 | LocationID string `gorm:"index"` // (txID,voutIndex)updated after being transfered 15 | Bitworkc string 16 | Bitworkr string 17 | 18 | Type string 19 | Subtype string 20 | TickerName string `gorm:"uniqueindex"` 21 | MaxSupply int64 22 | MintAmount int64 23 | MintHeight int64 24 | MaxMints int64 25 | } 26 | 27 | func (*GlobalDirectFt) TableName() string { 28 | return globalDirectFtTableName 29 | } 30 | 31 | func (*GlobalDirectFt) Init(db *gorm.DB) { 32 | var err error 33 | dmodel := newDefaultModel(globalDirectFtTableName, db) 34 | err = dmodel.DropTable() 35 | assert.Nil(nil, err) 36 | err = dmodel.CreateTable(&GlobalDirectFt{}) 37 | assert.Nil(nil, err) 38 | } 39 | 40 | func (*GlobalDirectFt) AutoMigrate(db *gorm.DB) { 41 | err := db.AutoMigrate(GlobalDirectFt{}) 42 | assert.Nil(nil, err) 43 | } 44 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsGlobalDistributedFt.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const globalDistributedFtTableName = "atomicals_global_distributed_ft" 9 | 10 | type GlobalDistributedFt struct { 11 | gorm.Model 12 | AtomicalsID string 13 | LocationID string 14 | TickerName string `gorm:"uniqueindex"` 15 | Type string 16 | Subtype string 17 | MintMode string // emu: perpetual, fixed 18 | MaxMintsGlobal int64 // total mint times allowed 19 | MintAmount int64 // mint amount once 20 | MaxMints int64 // # In the fixed mode there is a max number of mints allowed and then no more, only used when mintMode="fixed" 21 | MaxSupply int64 // total supply = MaxMintsGlobal*MintAmount 22 | MintHeight int64 // start mint height 23 | MintedTimes int64 // record minted times 24 | MintBitworkc string 25 | MintBitworkr string 26 | Bitworkc string 27 | Bitworkr string 28 | // Meta *witness.Meta 29 | Md string // emu:"", "0", "1" 30 | Bv string // mint_info['$mint_bitwork_vec'] = bv 31 | Bci string // mint_info['$mint_bitworkc_inc'] = bci 32 | Bri string // mint_info['$mint_bitworkr_inc'] = bri 33 | Bcs int64 // mint_info['$mint_bitworkc_start'] = bcs 34 | Brs int64 // mint_info['$mint_bitworkr_start'] = brs 35 | Maxg int64 36 | CommitHeight int64 37 | } 38 | 39 | func (*GlobalDistributedFt) TableName() string { 40 | return globalDistributedFtTableName 41 | } 42 | 43 | func (*GlobalDistributedFt) Init(db *gorm.DB) { 44 | var err error 45 | dmodel := newDefaultModel(globalDistributedFtTableName, db) 46 | err = dmodel.DropTable() 47 | assert.Nil(nil, err) 48 | err = dmodel.CreateTable(&GlobalDistributedFt{}) 49 | assert.Nil(nil, err) 50 | } 51 | 52 | func (*GlobalDistributedFt) AutoMigrate(db *gorm.DB) { 53 | err := db.AutoMigrate(GlobalDistributedFt{}) 54 | assert.Nil(nil, err) 55 | } 56 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsLocation.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const LocationTableName = "location" 9 | const LocationKey = "atomicals" 10 | 11 | type Location struct { 12 | gorm.Model 13 | Key string `gorm:"uniqueindex"` 14 | BlockHeight int64 15 | TxIndex int64 16 | Txid string 17 | } 18 | 19 | func (*Location) TableName() string { 20 | return LocationTableName 21 | } 22 | 23 | func (*Location) Init(db *gorm.DB) { 24 | var err error 25 | dmodel := newDefaultModel(LocationTableName, db) 26 | err = dmodel.DropTable() 27 | assert.Nil(nil, err) 28 | err = dmodel.CreateTable(&Location{}) 29 | assert.Nil(nil, err) 30 | } 31 | 32 | func (*Location) AutoMigrate(db *gorm.DB) { 33 | err := db.AutoMigrate(Location{}) 34 | assert.Nil(nil, err) 35 | } 36 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsMod.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const ModTableName = "atomicals_mod" 9 | 10 | type ModInfo struct { 11 | gorm.Model 12 | Height int64 13 | AtomicalsID string `gorm:"index"` // txID _ VOUT_EXPECT_OUTPUT_INDEX when be minted 14 | LocationID string `gorm:"index"` // txID_voutIndex updated after being transfered 15 | Mod string 16 | ModStr string 17 | } 18 | 19 | func (*ModInfo) TableName() string { 20 | return ModTableName 21 | } 22 | 23 | func (*ModInfo) Init(db *gorm.DB) { 24 | var err error 25 | dmodel := newDefaultModel(ModTableName, db) 26 | err = dmodel.DropTable() 27 | assert.Nil(nil, err) 28 | err = dmodel.CreateTable(&ModInfo{}) 29 | assert.Nil(nil, err) 30 | } 31 | 32 | func (*ModInfo) AutoMigrate(db *gorm.DB) { 33 | err := db.AutoMigrate(ModInfo{}) 34 | assert.Nil(nil, err) 35 | } 36 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsPayment.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const PaymentTableName = "atomicals_payment" 9 | 10 | type PaymentInfo struct { 11 | gorm.Model 12 | Height int64 13 | AtomicalsID string `gorm:"index"` // txID _ VOUT_EXPECT_OUTPUT_INDEX when be minted 14 | LocationID string `gorm:"index"` // txID_voutIndex updated after being transfered 15 | Payment string 16 | PaymentStr string 17 | } 18 | 19 | func (*PaymentInfo) TableName() string { 20 | return PaymentTableName 21 | } 22 | 23 | func (*PaymentInfo) Init(db *gorm.DB) { 24 | var err error 25 | dmodel := newDefaultModel(PaymentTableName, db) 26 | err = dmodel.DropTable() 27 | assert.Nil(nil, err) 28 | err = dmodel.CreateTable(&PaymentInfo{}) 29 | assert.Nil(nil, err) 30 | } 31 | 32 | func (*PaymentInfo) AutoMigrate(db *gorm.DB) { 33 | err := db.AutoMigrate(PaymentInfo{}) 34 | assert.Nil(nil, err) 35 | } 36 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsTx.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const atomicalsTxTableName = "atomicals_tx" 9 | 10 | type AtomicalsTx struct { 11 | gorm.Model 12 | BlockHeight int64 `gorm:"index"` 13 | TxIndex int64 14 | TxID string `gorm:"uniqueindex"` 15 | Operation string 16 | Description string 17 | } 18 | 19 | func (*AtomicalsTx) TableName() string { 20 | return atomicalsTxTableName 21 | } 22 | 23 | func (*AtomicalsTx) Init(db *gorm.DB) { 24 | var err error 25 | dmodel := newDefaultModel(atomicalsTxTableName, db) 26 | err = dmodel.DropTable() 27 | assert.Nil(nil, err) 28 | err = dmodel.CreateTable(&AtomicalsTx{}) 29 | assert.Nil(nil, err) 30 | } 31 | 32 | func (*AtomicalsTx) AutoMigrate(db *gorm.DB) { 33 | err := db.AutoMigrate(AtomicalsTx{}) 34 | assert.Nil(nil, err) 35 | } 36 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsUTXOFt.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const UserFtInfoTableName = "atomicals_utxo_ft" 9 | 10 | type FtType string 11 | 12 | const ( 13 | TypeDistributedFt FtType = "distributedFt" 14 | TypeDirectFt FtType = "directFt" 15 | ) 16 | 17 | type UTXOFtInfo struct { 18 | gorm.Model 19 | UserPk string 20 | AtomicalsID string `gorm:"index"` // (txID,VOUT_EXPECT_OUTPUT_INDEX) init when be minted 21 | LocationID string `gorm:"index"` // (txID,voutIndex)updated after being transfered 22 | Bitworkc string 23 | Bitworkr string 24 | 25 | // DistributedFt 26 | MintTicker string `gorm:"index"` 27 | Time int64 28 | MintBitworkVec string 29 | MintBitworkcInc string 30 | MintBitworkrInc string 31 | Amount int64 32 | 33 | // DirectFt 34 | Type string 35 | Subtype string 36 | TickerName string 37 | MaxSupply int64 38 | MintAmount int64 39 | MintHeight int64 40 | MaxMints int64 41 | } 42 | 43 | func (*UTXOFtInfo) TableName() string { 44 | return UserFtInfoTableName 45 | } 46 | 47 | func (*UTXOFtInfo) Init(db *gorm.DB) { 48 | var err error 49 | dmodel := newDefaultModel(UserFtInfoTableName, db) 50 | err = dmodel.DropTable() 51 | assert.Nil(nil, err) 52 | err = dmodel.CreateTable(&UTXOFtInfo{}) 53 | assert.Nil(nil, err) 54 | } 55 | 56 | func (*UTXOFtInfo) AutoMigrate(db *gorm.DB) { 57 | err := db.AutoMigrate(UTXOFtInfo{}) 58 | assert.Nil(nil, err) 59 | } 60 | -------------------------------------------------------------------------------- /repo/postsql/atomicalsUTXONft.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/atomicals-go/atomicals-indexer/atomicals-core/witness" 5 | "github.com/stretchr/testify/assert" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const UserNftInfoTableName = "atomicals_utxo_nft" 10 | 11 | type NftType string 12 | 13 | const ( 14 | TypeNftRealm NftType = "Realm" 15 | TypeNftSubRealm NftType = "SubRealm" 16 | TypeNftContainer NftType = "NftContainer" 17 | TypeNftItem NftType = "NftItem" 18 | ) 19 | 20 | type UTXONftInfo struct { 21 | gorm.Model 22 | UserPk string 23 | AtomicalsID string `gorm:"index"` // txID _ VOUT_EXPECT_OUTPUT_INDEX when be minted 24 | CommitHeight int64 25 | CommitTxIndex int64 26 | LocationID string `gorm:"index"` // txID_voutIndex updated after being transfered 27 | 28 | // realm 29 | RealmName string `gorm:"index"` 30 | // subRealm 31 | SubRealmName string `gorm:"index"` 32 | ClaimType witness.NftSubrealmClaimType 33 | ParentRealmAtomicalsID string 34 | 35 | // container 36 | ContainerName string `gorm:"index"` 37 | // Dmitem 38 | Dmitem string `gorm:"index"` 39 | ParentContainerAtomicalsID string 40 | 41 | Time int64 42 | Bitworkc string 43 | Bitworkr string 44 | } 45 | 46 | func (*UTXONftInfo) TableName() string { 47 | return UserNftInfoTableName 48 | } 49 | 50 | func (*UTXONftInfo) Init(db *gorm.DB) { 51 | var err error 52 | dmodel := newDefaultModel(UserNftInfoTableName, db) 53 | err = dmodel.DropTable() 54 | assert.Nil(nil, err) 55 | err = dmodel.CreateTable(&UTXONftInfo{}) 56 | assert.Nil(nil, err) 57 | } 58 | 59 | func (*UTXONftInfo) AutoMigrate(db *gorm.DB) { 60 | err := db.AutoMigrate(UTXONftInfo{}) 61 | assert.Nil(nil, err) 62 | } 63 | -------------------------------------------------------------------------------- /repo/postsql/bloomFilter.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/bits-and-blooms/bloom/v3" 5 | "github.com/stretchr/testify/assert" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const ( 10 | NftFilter = "nft" 11 | FtFilter = "ft" 12 | NftLocationFilter = "nft_locationID" 13 | FtLocationFilter = "ft_locationID" 14 | ) 15 | 16 | const BloomFilterTableName = "bloomFilter" 17 | 18 | type BloomFilter struct { 19 | gorm.Model 20 | Name string `gorm:"uniqueindex"` 21 | Data []byte 22 | } 23 | 24 | func (*BloomFilter) TableName() string { 25 | return BloomFilterTableName 26 | } 27 | 28 | func (*BloomFilter) Init(db *gorm.DB) { 29 | var err error 30 | dmodel := newDefaultModel(BloomFilterTableName, db) 31 | err = dmodel.DropTable() 32 | assert.Nil(nil, err) 33 | err = dmodel.CreateTable(&BloomFilter{}) 34 | assert.Nil(nil, err) 35 | 36 | filter := bloom.NewWithEstimates(10000, 0.01) 37 | data, err := filter.MarshalJSON() 38 | assert.Nil(nil, err) 39 | dbTx := db.Save(&BloomFilter{Name: NftFilter, Data: data}) 40 | 41 | assert.Nil(nil, dbTx.Error) 42 | filter = bloom.NewWithEstimates(10000, 0.01) 43 | data, err = filter.MarshalJSON() 44 | assert.Nil(nil, err) 45 | dbTx = db.Save(&BloomFilter{Name: FtFilter, Data: data}) 46 | assert.Nil(nil, dbTx.Error) 47 | 48 | filter = bloom.NewWithEstimates(70000, 0.01) 49 | data, err = filter.MarshalJSON() 50 | assert.Nil(nil, err) 51 | dbTx = db.Save(&BloomFilter{Name: NftLocationFilter, Data: data}) 52 | assert.Nil(nil, dbTx.Error) 53 | 54 | filter = bloom.NewWithEstimates(200000, 0.01) 55 | data, err = filter.MarshalJSON() 56 | assert.Nil(nil, err) 57 | dbTx = db.Save(&BloomFilter{Name: FtLocationFilter, Data: data}) 58 | assert.Nil(nil, dbTx.Error) 59 | } 60 | 61 | func (*BloomFilter) AutoMigrate(db *gorm.DB) { 62 | err := db.AutoMigrate(BloomFilter{}) 63 | assert.Nil(nil, err) 64 | } 65 | -------------------------------------------------------------------------------- /repo/postsql/init/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/atomicals-go/pkg/conf" 5 | "github.com/atomicals-go/repo/postsql" 6 | "gorm.io/driver/postgres" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func InitModels(db *gorm.DB) { 11 | (&postsql.GlobalDirectFt{}).Init(db) 12 | (&postsql.GlobalDistributedFt{}).Init(db) 13 | (&postsql.UTXOFtInfo{}).Init(db) 14 | (&postsql.UTXONftInfo{}).Init(db) 15 | (&postsql.ModInfo{}).Init(db) 16 | (&postsql.DatInfo{}).Init(db) 17 | (&postsql.PaymentInfo{}).Init(db) 18 | (&postsql.AtomicalsTx{}).Init(db) 19 | (&postsql.Location{}).Init(db) 20 | (&postsql.BloomFilter{}).Init(db) 21 | (&postsql.StatisticTx{}).Init(db) 22 | } 23 | 24 | func AutoMigrate(db *gorm.DB) { 25 | (&postsql.GlobalDirectFt{}).AutoMigrate(db) 26 | (&postsql.GlobalDistributedFt{}).AutoMigrate(db) 27 | (&postsql.UTXOFtInfo{}).AutoMigrate(db) 28 | (&postsql.UTXONftInfo{}).AutoMigrate(db) 29 | (&postsql.ModInfo{}).AutoMigrate(db) 30 | (&postsql.DatInfo{}).AutoMigrate(db) 31 | (&postsql.PaymentInfo{}).AutoMigrate(db) 32 | (&postsql.AtomicalsTx{}).AutoMigrate(db) 33 | (&postsql.Location{}).AutoMigrate(db) 34 | (&postsql.BloomFilter{}).AutoMigrate(db) 35 | (&postsql.StatisticTx{}).AutoMigrate(db) 36 | } 37 | 38 | func main() { 39 | conf, err := conf.ReadJSONFromJSFile("../../../conf/config.json") 40 | if err != nil { 41 | panic(err) 42 | } 43 | DB, err := gorm.Open(postgres.Open(conf.SqlDNS), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}) 44 | if err != nil { 45 | panic(err) 46 | } 47 | InitModels(DB) 48 | AutoMigrate(DB) 49 | } 50 | -------------------------------------------------------------------------------- /repo/postsql/model.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import "gorm.io/gorm" 4 | 5 | type DefaultModelImpl struct { 6 | Table string 7 | DB *gorm.DB 8 | } 9 | 10 | func newDefaultModel(table string, db *gorm.DB) *DefaultModelImpl { 11 | return &DefaultModelImpl{ 12 | Table: table, 13 | DB: db, 14 | } 15 | } 16 | 17 | func (m *DefaultModelImpl) DropTable() error { 18 | return m.DB.Migrator().DropTable(m.Table) 19 | } 20 | func (m *DefaultModelImpl) CreateTable(model interface{}) error { 21 | return m.DB.AutoMigrate(model) 22 | } 23 | -------------------------------------------------------------------------------- /repo/postsql/statistic.go: -------------------------------------------------------------------------------- 1 | package postsql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | const statisticTxTableName = "statistic_tx" 9 | 10 | type StatisticTx struct { 11 | gorm.Model 12 | BlockHeight int64 `gorm:"uniqueindex"` 13 | Ticker string 14 | UserPk string 15 | Amount int 16 | Description string 17 | } 18 | 19 | func (*StatisticTx) TableName() string { 20 | return statisticTxTableName 21 | } 22 | 23 | func (*StatisticTx) Init(db *gorm.DB) { 24 | var err error 25 | dmodel := newDefaultModel(statisticTxTableName, db) 26 | err = dmodel.DropTable() 27 | assert.Nil(nil, err) 28 | err = dmodel.CreateTable(&StatisticTx{}) 29 | assert.Nil(nil, err) 30 | } 31 | 32 | func (*StatisticTx) AutoMigrate(db *gorm.DB) { 33 | err := db.AutoMigrate(StatisticTx{}) 34 | assert.Nil(nil, err) 35 | } 36 | -------------------------------------------------------------------------------- /testdata/mainnet.md: -------------------------------------------------------------------------------- 1 | // container toothy 74c5f65bafbf9281456aa8820dc06b5ce17463bce34011febba9f7379f889651 2 | // height, txIndex, tx := m.GetTxByHeightAndIndex(int64(812481), int64(1627)) 3 | // m.traceTx(tx, height, txIndex) 4 | 5 | // // mod 6 | // height, txIndex, tx = m.GetTxByHeightAndIndex(int64(812547), int64(2037)) // txid d4fb663afbeac3cd5b7e826cdd55fa577a9716bdaa53efb2f18f7518f61e313b 7 | // m.traceTx(tx, height, txIndex) 8 | // height, txIndex, tx = m.GetTxByHeightAndIndex(int64(816734), int64(3332)) // txid 80481aa37fb7b41f7d59b3430a07632144c3c45dce25c73b6a11c5e9cf91f911 9 | // m.traceTx(tx, height, txIndex) 10 | // height, txIndex, tx = m.GetTxByHeightAndIndex(int64(816829), int64(1534)) // txid cdfcd124238782766078dd48731883ed84beb1a2a6a4db87212e4b91af514927 11 | // m.traceTx(tx, height, txIndex) 12 | 13 | // // nft 14 | // height, txIndex, tx := m.GetTxByHeightAndIndex(int64(819181), int64(5)) // txid 34647249501dd22e99eca69ae62dc0d7dbab8b2a31f3b792968db5b49328f765 15 | // m.traceTx(tx, height, txIndex) 16 | 17 | 18 | // realm 19 | height, txIndex, tx := m.GetTxByHeightAndIndex(int64(812481), int64(1448)) // txid 818e827372725c488e94d658aeb4269b123f1f6e8f36dd10906573d1bf9f45ad 20 | m.traceTx(tx, height, txIndex) 21 | height, txIndex, tx = m.GetTxByHeightAndIndex(int64(812547), int64(2038)) // txid 4211d0c9b069f1c9624b9616c6ea0c0c548d8beceede393c938d09eb4e971a47 22 | m.traceTx(tx, height, txIndex) -------------------------------------------------------------------------------- /utils/atomicalsID.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func AtomicalsID(txID string, voutIndex int64) string { 10 | return fmt.Sprintf("%vi%v", txID, voutIndex) 11 | } 12 | 13 | func SplitAtomicalsID(atomicalsID string) (string, string) { 14 | parts := strings.SplitN(atomicalsID, "i", 2) 15 | if len(parts) == 2 { 16 | return parts[0], parts[1] 17 | } 18 | return "", "" 19 | } 20 | 21 | // is_compact_atomical_id 22 | func IsCompactAtomicalID(value string) bool { 23 | // Check if the value is an integer 24 | // Go doesn't have a built-in 'isinstance' function like Python, so we check if we can convert it to an integer 25 | var intValue int 26 | _, err := fmt.Sscan(value, &intValue) 27 | if err == nil { 28 | return false 29 | } 30 | 31 | // Check if the value is empty or None 32 | if value == "" { 33 | return false 34 | } 35 | 36 | // Check if the length is at least 64 characters and the 64th character is 'i' 37 | if len(value) < 64 || value[63] != 'i' { 38 | return false 39 | } 40 | 41 | // Extract the raw hash part and convert it to bytes 42 | rawHashHex := value[:64] 43 | rawHash, err := hex.DecodeString(rawHashHex) 44 | if err != nil { 45 | return false 46 | } 47 | 48 | // Check if the raw hash has a length of 32 bytes 49 | return len(rawHash) == 32 50 | } 51 | -------------------------------------------------------------------------------- /utils/bitwork.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func ParseMintBitwork(commitTxID, mintBitworkc, mintBitworkr string) (*Bitwork, *Bitwork, error) { 12 | bitworkc := ParseBitwork(mintBitworkc) 13 | bitworkr := ParseBitwork(mintBitworkr) 14 | return bitworkc, bitworkr, nil 15 | } 16 | 17 | // is_proof_of_work_prefix_match 18 | func IsProofOfWorkPrefixMatch(txID string, powPrefix string, powPrefixExt int) bool { 19 | if powPrefixExt < 0 { 20 | return strings.HasPrefix(txID, powPrefix) 21 | } 22 | if powPrefixExt < 0 || powPrefixExt > 15 { 23 | return false 24 | } 25 | 26 | // Check that the main prefix matches 27 | initialTestMatchesMainPrefix := strings.HasPrefix(txID, powPrefix) 28 | if !initialTestMatchesMainPrefix { 29 | return false 30 | } 31 | 32 | // If there is an extended powPrefixExt, then we require it to validate the POW 33 | if powPrefixExt > 0 { 34 | // Check that the next digit is within the range of powPrefixExt 35 | nextChar := txID[len(powPrefix)] 36 | charMap := map[rune]int{ 37 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, 38 | '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 39 | 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15, 40 | } 41 | getNumericValue := charMap[rune(nextChar)] 42 | 43 | // powPrefixExt == 0 is functionally equivalent to not having a powPrefixExt 44 | // powPrefixExt == 15 is functionally equivalent to extending the powPrefix by 1 45 | return getNumericValue >= powPrefixExt 46 | } 47 | 48 | // There is no extended powPrefixExt, and we just apply the main prefix 49 | return true 50 | } 51 | 52 | // # Parse a bitwork stirng such as '123af.15' 53 | type Bitwork struct { 54 | Prefix string 55 | Ext int 56 | } 57 | 58 | // is_valid_bitwork_string 59 | func ParseBitwork(bitwork string) *Bitwork { 60 | if bitwork == "" { 61 | return nil 62 | } 63 | if strings.Count(bitwork, ".") > 1 { 64 | return nil 65 | } 66 | splitted := strings.Split(bitwork, ".") 67 | prefix := splitted[0] 68 | ext := -1 69 | if len(splitted) > 1 { 70 | extStr := splitted[1] 71 | extInt, err := strconv.Atoi(extStr) 72 | if err != nil { 73 | return nil 74 | } 75 | ext = extInt 76 | } 77 | if prefix == "" { 78 | return nil 79 | } 80 | if !regexp.MustCompile("^[a-f0-9]{1,64}$").MatchString(prefix) { 81 | return nil 82 | } 83 | if ext > 15 { 84 | return nil 85 | } 86 | return &Bitwork{ 87 | Prefix: prefix, 88 | Ext: ext, 89 | } 90 | } 91 | 92 | func GetNextBitworkFullStr(bitworkVec string, currentPrefixLen int) string { 93 | baseBitworkPadded := fmt.Sprintf("%-32s", bitworkVec) 94 | if currentPrefixLen >= 31 { 95 | return baseBitworkPadded 96 | } 97 | return baseBitworkPadded[:currentPrefixLen+1] 98 | } 99 | 100 | func IsMintPowValid(txid, mintPowCommit string) bool { 101 | bitworkCommitParts := ParseBitwork(mintPowCommit) 102 | if bitworkCommitParts == nil { 103 | return false 104 | } 105 | mintBitworkPrefix := bitworkCommitParts.Prefix 106 | mintBitworkExt := bitworkCommitParts.Ext 107 | return IsProofOfWorkPrefixMatch(txid, mintBitworkPrefix, mintBitworkExt) 108 | } 109 | 110 | func Calculate_expected_bitwork(bitwork_vec string, actual_mints, max_mints, target_increment, starting_target int64) string { 111 | if starting_target < 64 || starting_target > 256 { 112 | panic("err") 113 | } 114 | if max_mints < 1 || max_mints > 100000 { 115 | panic("err") 116 | } 117 | if target_increment < 1 || target_increment > 64 { 118 | panic("err") 119 | } 120 | target_steps := (actual_mints) / (max_mints) 121 | current_target := starting_target + (target_steps * target_increment) 122 | return derive_bitwork_prefix_from_target(bitwork_vec, current_target) 123 | } 124 | 125 | func derive_bitwork_prefix_from_target(baseBitworkPrefix string, target int64) string { 126 | if target < 16 { 127 | panic(fmt.Sprintf("increments must be at least 16. Provided: %d", target)) 128 | } 129 | 130 | baseBitworkPadded := fmt.Sprintf("%-32s", baseBitworkPrefix) 131 | multiples := float64(target) / 16 132 | fullAmount := int(math.Floor(multiples)) 133 | modulo := target % 16 134 | 135 | bitworkPrefix := baseBitworkPadded 136 | if fullAmount < 32 { 137 | bitworkPrefix = baseBitworkPadded[:fullAmount] 138 | } 139 | 140 | if modulo > 0 { 141 | return bitworkPrefix + "." + fmt.Sprint(modulo) 142 | } 143 | 144 | return bitworkPrefix 145 | } 146 | -------------------------------------------------------------------------------- /utils/blockHeight.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // is_dmint_activated sort_fifo 4 | func IsDmintActivated(height int64) bool { 5 | return height >= ATOMICALS_ACTIVATION_HEIGHT_DMINT 6 | } 7 | 8 | func IsCustomColoring(height int64) bool { 9 | return height >= ATOMICALS_ACTIVATION_HEIGHT_CUSTOM_COLORING 10 | } 11 | 12 | func Is_within_acceptable_blocks_for_sub_item_payment(commit_height, current_height int64) bool { 13 | return current_height <= commit_height+MINT_SUBNAME_COMMIT_PAYMENT_DELAY_BLOCKS 14 | } 15 | 16 | func Is_density_activated(height int64) bool { 17 | return height >= ATOMICALS_ACTIVATION_HEIGHT_DENSITY 18 | } 19 | -------------------------------------------------------------------------------- /utils/byte.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "errors" 7 | ) 8 | 9 | func UnpackLeUint16From(b []byte) uint16 { 10 | return uint16(b[0]) | uint16(b[1])<<8 11 | } 12 | 13 | func UnpackLeUint32From(b []byte) uint32 { 14 | return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 15 | } 16 | 17 | func parseLEUint32(s string) (uint32, error) { 18 | bytes, err := hex.DecodeString(s) 19 | if err != nil { 20 | return 0, err 21 | } 22 | if len(bytes) != 4 { 23 | return 0, errors.New("invalid length for LE uint32") 24 | } 25 | return binary.LittleEndian.Uint32(bytes), nil 26 | } 27 | 28 | func packLEUint32(num uint32) []byte { 29 | bytes := make([]byte, 4) 30 | binary.LittleEndian.PutUint32(bytes, num) 31 | return bytes 32 | } 33 | -------------------------------------------------------------------------------- /utils/constant.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/shopspring/decimal" 4 | 5 | var ( 6 | Satoshi = decimal.NewFromFloat(1e+08) //1BTC 7 | SatoshiSqrt = decimal.NewFromFloat(10000.0) //1BTC 8 | ) 9 | 10 | const ( 11 | SafeBlockHeightInterupt = 3 12 | ) 13 | 14 | const ( 15 | ATOMICALS_ENVELOPE_MARKER_BYTES = "0461746f6d" 16 | MINT_REALM_CONTAINER_TICKER_COMMIT_REVEAL_DELAY_BLOCKS = 3 17 | MINT_GENERAL_COMMIT_REVEAL_DELAY_BLOCKS = 100 18 | MINT_SUBNAME_COMMIT_PAYMENT_DELAY_BLOCKS = 15 // # ~2.5 hours. 19 | MINT_SUBNAME_RULES_BECOME_EFFECTIVE_IN_BLOCKS = 1 20 | MAX_SUBNAME_RULE_SIZE_LEN = 100000 21 | MAX_SUBNAME_RULE_ENTRIES = 100 22 | DFT_MINT_AMOUNT_MIN = 546 23 | DFT_MINT_AMOUNT_MAX = 100000000 24 | DFT_MINT_MAX_MIN_COUNT = 1 25 | DFT_MINT_MAX_MAX_COUNT_LEGACY = 500000 26 | DFT_MINT_MAX_MAX_COUNT_DENSITY = 21000000 27 | DFT_MINT_HEIGHT_MIN = 0 28 | DFT_MINT_HEIGHT_MAX = 10000000 29 | VOUT_EXPECT_OUTPUT_INDEX = 0 30 | DMINT_PATH = "dmint" 31 | SUBNAME_MIN_PAYMENT_DUST_LIMIT = 0 // # It can be possible to do free 32 | ATOMICALS_ACTIVATION_HEIGHT = 808080 33 | ATOMICALS_ACTIVATION_HEIGHT_DMINT = 819181 34 | ATOMICALS_ACTIVATION_HEIGHT_COMMITZ = 822800 35 | ATOMICALS_ACTIVATION_HEIGHT_DENSITY = 828128 36 | ATOMICALS_ACTIVATION_HEIGHT_DFT_BITWORK_ROLLOVER = 828628 37 | ATOMICALS_ACTIVATION_HEIGHT_CUSTOM_COLORING = 848484 38 | ) 39 | -------------------------------------------------------------------------------- /utils/name.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // is_valid_ticker_string 9 | func IsValidTicker(ticker string) bool { 10 | if ticker == "" { 11 | return false 12 | } 13 | m := regexp.MustCompile(`^[a-z0-9]{1,21}$`) 14 | return m.MatchString(ticker) 15 | } 16 | 17 | // is_valid_realm_string_name 18 | func IsValidRealm(realmName string) bool { 19 | if realmName == "" { 20 | return false 21 | } 22 | if strings.HasPrefix(realmName, "-") { 23 | return false 24 | } 25 | if strings.HasSuffix(realmName, "-") { 26 | return false 27 | } 28 | if len(realmName) > 64 || len(realmName) <= 0 { 29 | return false 30 | } 31 | // # Realm names must start with an alphabetical character 32 | m := regexp.MustCompile(`^[a-z][a-z0-9\-]{0,63}$`) 33 | return m.MatchString(realmName) 34 | } 35 | 36 | // is_valid_subrealm_string_name 37 | func IsValidSubRealm(realmName string) bool { 38 | if realmName == "" { 39 | return false 40 | } 41 | if strings.HasPrefix(realmName, "-") { 42 | return false 43 | } 44 | if strings.HasSuffix(realmName, "-") { 45 | return false 46 | } 47 | if len(realmName) > 64 || len(realmName) <= 0 { 48 | return false 49 | } 50 | // # Realm names must start with an alphabetical character 51 | m := regexp.MustCompile(`^[a-z0-9][a-z0-9\-]{0,63}$`) 52 | return m.MatchString(realmName) 53 | } 54 | 55 | // is_valid_container_string_name 56 | func IsValidContainer(containerName string) bool { 57 | if containerName == "" { 58 | return false 59 | } 60 | if strings.HasPrefix(containerName, "-") { 61 | return false 62 | } 63 | if strings.HasSuffix(containerName, "-") { 64 | return false 65 | } 66 | if len(containerName) > 64 || len(containerName) <= 0 { 67 | return false 68 | } 69 | // # Realm names must start with an alphabetical character 70 | m := regexp.MustCompile(`^[a-z0-9][a-z0-9\-]{0,63}$`) 71 | return m.MatchString(containerName) 72 | } 73 | 74 | // is_valid_container_dmitem_string_name 75 | func IsValidDmitem(dmitemName string) bool { 76 | if dmitemName == "" { 77 | return false 78 | } 79 | if strings.HasPrefix(dmitemName, "-") { 80 | return false 81 | } 82 | if strings.HasSuffix(dmitemName, "-") { 83 | return false 84 | } 85 | if len(dmitemName) > 64 || len(dmitemName) <= 0 { 86 | return false 87 | } 88 | m := regexp.MustCompile(`^[a-z0-9][a-z0-9\-]{0,63}$`) 89 | return m.MatchString(dmitemName) 90 | } 91 | 92 | // is_hex_string_regex 93 | func IsHexStringRegex(value string) bool { 94 | m := regexp.MustCompile(`^[a-z0-9]+$`) 95 | return m.MatchString(value) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /utils/scriptPubKey.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | ) 7 | 8 | // is_unspendable_legacy 9 | func IsUnspendableLegacy(script []byte) bool { 10 | // OP_FALSE OP_RETURN or OP_RETURN 11 | return bytes.Equal(script[:2], []byte{0x00, 0x6a}) || (len(script) > 0 && script[0] == 0x6a) 12 | } 13 | 14 | // is_unspendable_genesis 15 | func IsUnspendableGenesis(script []byte) bool { 16 | // OP_FALSE OP_RETURN 17 | return bytes.Equal(script[:2], []byte{0x00, 0x6a}) 18 | } 19 | 20 | func Is_op_return_subrealm_payment_marker_atomical_id(script []byte) string { 21 | if len(script) < 1+5+2+1+36 { // 6a04<01>p 22 | return "" 23 | } 24 | // Ensure it is an OP_RETURN 25 | firstByte := script[0] 26 | secondBytes := script[:2] 27 | if secondBytes[0] != 0x00 && firstByte != 0x6a { 28 | return "" 29 | } 30 | startIndex := 1 31 | if secondBytes[0] == 0x00 { 32 | startIndex = 2 33 | } 34 | // Check for the envelope format 35 | if hex.EncodeToString(script[startIndex:startIndex+5]) != ATOMICALS_ENVELOPE_MARKER_BYTES { 36 | return "" 37 | } 38 | // Check the next op code matches 'p' for payment 39 | if hex.EncodeToString(script[startIndex+5:startIndex+5+2]) != "0170" { 40 | return "" 41 | } 42 | // Check there is a 36 byte push data 43 | if hex.EncodeToString(script[startIndex+5+2:startIndex+5+2+1]) != "24" { 44 | return "" 45 | } 46 | // Extract and return the atomical ID 47 | atomicalID := script[startIndex+5+2+1 : startIndex+5+2+1+36] 48 | return hex.EncodeToString(atomicalID) 49 | } 50 | func Is_op_return_dmitem_payment_marker_atomical_id(script []byte) string { 51 | if len(script) < 1+5+2+1+36 { // 6a04<01>p 52 | return "" 53 | } 54 | // Ensure it is an OP_RETURN 55 | firstByte := script[0] 56 | secondBytes := script[:2] 57 | if secondBytes[0] != 0x00 && firstByte != 0x6a { 58 | return "" 59 | } 60 | startIndex := 1 61 | if secondBytes[0] == 0x00 { 62 | startIndex = 2 63 | } 64 | // Check for the envelope format 65 | if hex.EncodeToString(script[startIndex:startIndex+5]) != ATOMICALS_ENVELOPE_MARKER_BYTES { 66 | return "" 67 | } 68 | // Check the next op code matches 'p' for payment 69 | if hex.EncodeToString(script[startIndex+5:startIndex+5+2]) != "0164" { 70 | return "" 71 | } 72 | // Check there is a 36 byte push data 73 | if hex.EncodeToString(script[startIndex+5+2:startIndex+5+2+1]) != "24" { 74 | return "" 75 | } 76 | // Extract and return the atomical ID 77 | atomicalID := script[startIndex+5+2+1 : startIndex+5+2+1+36] 78 | return hex.EncodeToString(atomicalID) 79 | } 80 | -------------------------------------------------------------------------------- /utils/sha256/constant.go: -------------------------------------------------------------------------------- 1 | package sha 2 | 3 | var k = _K 4 | 5 | var _K = []uint32{ 6 | 0x428a2f98, 7 | 0x71374491, 8 | 0xb5c0fbcf, 9 | 0xe9b5dba5, 10 | 0x3956c25b, 11 | 0x59f111f1, 12 | 0x923f82a4, 13 | 0xab1c5ed5, 14 | 0xd807aa98, 15 | 0x12835b01, 16 | 0x243185be, 17 | 0x550c7dc3, 18 | 0x72be5d74, 19 | 0x80deb1fe, 20 | 0x9bdc06a7, 21 | 0xc19bf174, 22 | 0xe49b69c1, 23 | 0xefbe4786, 24 | 0x0fc19dc6, 25 | 0x240ca1cc, 26 | 0x2de92c6f, 27 | 0x4a7484aa, 28 | 0x5cb0a9dc, 29 | 0x76f988da, 30 | 0x983e5152, 31 | 0xa831c66d, 32 | 0xb00327c8, 33 | 0xbf597fc7, 34 | 0xc6e00bf3, 35 | 0xd5a79147, 36 | 0x06ca6351, 37 | 0x14292967, 38 | 0x27b70a85, 39 | 0x2e1b2138, 40 | 0x4d2c6dfc, 41 | 0x53380d13, 42 | 0x650a7354, 43 | 0x766a0abb, 44 | 0x81c2c92e, 45 | 0x92722c85, 46 | 0xa2bfe8a1, 47 | 0xa81a664b, 48 | 0xc24b8b70, 49 | 0xc76c51a3, 50 | 0xd192e819, 51 | 0xd6990624, 52 | 0xf40e3585, 53 | 0x106aa070, 54 | 0x19a4c116, 55 | 0x1e376c08, 56 | 0x2748774c, 57 | 0x34b0bcb5, 58 | 0x391c0cb3, 59 | 0x4ed8aa4a, 60 | 0x5b9cca4f, 61 | 0x682e6ff3, 62 | 0x748f82ee, 63 | 0x78a5636f, 64 | 0x84c87814, 65 | 0x8cc70208, 66 | 0x90befffa, 67 | 0xa4506ceb, 68 | 0xbef9a3f7, 69 | 0xc67178f2, 70 | } 71 | 72 | // The size of a SHA256 checksum in bytes. 73 | const Size = 32 74 | 75 | // The size of a SHA224 checksum in bytes. 76 | const Size224 = 28 77 | 78 | // The blocksize of SHA256 and SHA224 in bytes. 79 | const BlockSize = 64 80 | 81 | const ( 82 | chunk = 64 83 | init0 = 0x6A09E667 84 | init1 = 0xBB67AE85 85 | init2 = 0x3C6EF372 86 | init3 = 0xA54FF53A 87 | init4 = 0x510E527F 88 | init5 = 0x9B05688C 89 | init6 = 0x1F83D9AB 90 | init7 = 0x5BE0CD19 91 | init0_224 = 0xC1059ED8 92 | init1_224 = 0x367CD507 93 | init2_224 = 0x3070DD17 94 | init3_224 = 0xF70E5939 95 | init4_224 = 0xFFC00B31 96 | init5_224 = 0x68581511 97 | init6_224 = 0x64F98FA7 98 | init7_224 = 0xBEFA4FA4 99 | ) 100 | -------------------------------------------------------------------------------- /utils/sha256/math.go: -------------------------------------------------------------------------------- 1 | package sha 2 | 3 | func RotateLeft32(x uint32, k int) uint32 { 4 | const n = 32 5 | s := uint(k) & (n - 1) 6 | return x<>(n-s) 7 | } 8 | -------------------------------------------------------------------------------- /utils/sha256/sha.go: -------------------------------------------------------------------------------- 1 | package sha 2 | 3 | type digest struct { 4 | h [8]uint32 5 | x [chunk]byte 6 | nx int 7 | len uint64 8 | is224 bool // mark if this digest is SHA-224 9 | } 10 | 11 | // SHA256 computes the SHA256 hash of the input data 12 | func SHA256(data []byte) []byte { 13 | d := &digest{} 14 | d.Reset() 15 | 16 | d.Write(data) 17 | 18 | da := d.checkSum() 19 | 20 | return da[:] 21 | } 22 | 23 | func (d *digest) Reset() { 24 | d.h[0] = init0 25 | d.h[1] = init1 26 | d.h[2] = init2 27 | d.h[3] = init3 28 | d.h[4] = init4 29 | d.h[5] = init5 30 | d.h[6] = init6 31 | d.h[7] = init7 32 | 33 | d.nx = 0 34 | d.len = 0 35 | } 36 | 37 | func (d *digest) Write(p []byte) { 38 | nn := len(p) 39 | d.len += uint64(nn) 40 | if d.nx > 0 { 41 | n := copy(d.x[d.nx:], p) 42 | d.nx += n 43 | if d.nx == chunk { 44 | d.block(d.x[:]) 45 | d.nx = 0 46 | } 47 | p = p[n:] 48 | } 49 | if len(p) >= chunk { 50 | n := len(p) &^ (chunk - 1) 51 | d.block(p[:n]) 52 | p = p[n:] 53 | } 54 | if len(p) > 0 { 55 | d.nx = copy(d.x[:], p) 56 | } 57 | } 58 | 59 | func (dig *digest) block(p []byte) { 60 | var w [64]uint32 61 | h0, h1, h2, h3, h4, h5, h6, h7 := dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] 62 | for len(p) >= chunk { 63 | // Can interlace the computation of w with the 64 | // rounds below if needed for speed. 65 | for i := 0; i < 16; i++ { 66 | j := i * 4 67 | w[i] = uint32(p[j])<<24 | uint32(p[j+1])<<16 | uint32(p[j+2])<<8 | uint32(p[j+3]) 68 | } 69 | for i := 16; i < 64; i++ { 70 | v1 := w[i-2] 71 | t1 := (RotateLeft32(v1, -17)) ^ (RotateLeft32(v1, -19)) ^ (v1 >> 10) 72 | v2 := w[i-15] 73 | t2 := (RotateLeft32(v2, -7)) ^ (RotateLeft32(v2, -18)) ^ (v2 >> 3) 74 | w[i] = t1 + w[i-7] + t2 + w[i-16] 75 | } 76 | 77 | a, b, c, d, e, f, g, h := h0, h1, h2, h3, h4, h5, h6, h7 78 | 79 | for i := 0; i < 64; i++ { 80 | t1 := h + ((RotateLeft32(e, -6)) ^ (RotateLeft32(e, -11)) ^ (RotateLeft32(e, -25))) + ((e & f) ^ (^e & g)) + _K[i] + w[i] 81 | t2 := ((RotateLeft32(a, -2)) ^ (RotateLeft32(a, -13)) ^ (RotateLeft32(a, -22))) + ((a & b) ^ (a & c) ^ (b & c)) 82 | 83 | h = g 84 | g = f 85 | f = e 86 | e = d + t1 87 | d = c 88 | c = b 89 | b = a 90 | a = t1 + t2 91 | } 92 | 93 | h0 += a 94 | h1 += b 95 | h2 += c 96 | h3 += d 97 | h4 += e 98 | h5 += f 99 | h6 += g 100 | h7 += h 101 | 102 | p = p[chunk:] 103 | } 104 | 105 | dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] = h0, h1, h2, h3, h4, h5, h6, h7 106 | } 107 | -------------------------------------------------------------------------------- /utils/sha256/sum.go: -------------------------------------------------------------------------------- 1 | package sha 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | func (d *digest) checkSum() [Size]byte { 8 | len := d.len 9 | // Padding. Add a 1 bit and 0 bits until 56 bytes mod 64. 10 | var tmp [64 + 8]byte // padding + length buffer 11 | tmp[0] = 0x80 12 | var t uint64 13 | if len%64 < 56 { 14 | t = 56 - len%64 15 | } else { 16 | t = 64 + 56 - len%64 17 | } 18 | 19 | // Length in bits. 20 | len <<= 3 21 | padlen := tmp[:t+8] 22 | binary.BigEndian.PutUint64(padlen[t+0:], len) 23 | d.Write(padlen) 24 | 25 | if d.nx != 0 { 26 | panic("d.nx != 0") 27 | } 28 | 29 | var digest [Size]byte 30 | 31 | binary.BigEndian.PutUint32(digest[0:], d.h[0]) 32 | binary.BigEndian.PutUint32(digest[4:], d.h[1]) 33 | binary.BigEndian.PutUint32(digest[8:], d.h[2]) 34 | binary.BigEndian.PutUint32(digest[12:], d.h[3]) 35 | binary.BigEndian.PutUint32(digest[16:], d.h[4]) 36 | binary.BigEndian.PutUint32(digest[20:], d.h[5]) 37 | binary.BigEndian.PutUint32(digest[24:], d.h[6]) 38 | if !d.is224 { 39 | binary.BigEndian.PutUint32(digest[28:], d.h[7]) 40 | } 41 | 42 | return digest 43 | } 44 | -------------------------------------------------------------------------------- /utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | func IsHexString(value string) bool { 10 | // Try to decode the string as hexadecimal 11 | _, err := hex.DecodeString(value) 12 | if err != nil { 13 | return false 14 | } 15 | return true 16 | } 17 | 18 | func IsValidRegex(regex string) bool { 19 | if regex == "" { 20 | return false 21 | } 22 | 23 | if strings.ContainsAny(regex, "()") { 24 | return false 25 | } 26 | 27 | _, err := regexp.Compile(regex) 28 | if err != nil { 29 | return false 30 | } 31 | 32 | return true 33 | } 34 | 35 | func CompileRegex(pattern string) (*regexp.Regexp, error) { 36 | // Compile the regex pattern 37 | regex, err := regexp.Compile(pattern) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return regex, nil 42 | } 43 | -------------------------------------------------------------------------------- /utils/unused.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // Convert the compact string form to the expanded 36 byte sequence 10 | func compact_to_location_id_bytes(value string) ([]byte, error) { 11 | if value == "" { 12 | return nil, errors.New("value in compactToLocationIDBytes is not set") 13 | } 14 | 15 | indexOfI := len(value) - 1 16 | if indexOfI != 64 || value[indexOfI] != 'i' { 17 | return nil, fmt.Errorf("%s should be 32 bytes hex followed by i", value) 18 | } 19 | 20 | rawHash, err := hex.DecodeString(value[:64]) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | if len(rawHash) != 32 { 26 | return nil, fmt.Errorf("%s should be 32 bytes hex followed by i", value) 27 | } 28 | 29 | num, err := parseLEUint32(value[65:]) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if num < 0 || num > 100000 { 35 | return nil, fmt.Errorf("%s index output number was parsed to be less than 0 or greater than 100000", value) 36 | } 37 | return append(rawHash, packLEUint32(num)...), nil 38 | } 39 | 40 | func is_op_return_subrealm_payment_marker_atomical_id(script []byte) string { 41 | if len(script) < 1+5+2+1+36 { // 6a04<01>p 42 | return "" 43 | } 44 | // Ensure it is an OP_RETURN 45 | firstByte := script[0] 46 | secondBytes := script[:2] 47 | if secondBytes[0] != 0x00 && firstByte != 0x6a { 48 | return "" 49 | } 50 | startIndex := 1 51 | if secondBytes[0] == 0x00 { 52 | startIndex = 2 53 | } 54 | // Check for the envelope format 55 | if hex.EncodeToString(script[startIndex:startIndex+5]) != ATOMICALS_ENVELOPE_MARKER_BYTES { 56 | return "" 57 | } 58 | // Check the next op code matches 'p' for payment 59 | if hex.EncodeToString(script[startIndex+5:startIndex+5+2]) != "0170" { 60 | return "" 61 | } 62 | // Check there is a 36 byte push data 63 | if hex.EncodeToString(script[startIndex+5+2:startIndex+5+2+1]) != "24" { 64 | return "" 65 | } 66 | // Extract and return the atomical ID 67 | atomicalID := script[startIndex+5+2+1 : startIndex+5+2+1+36] 68 | return hex.EncodeToString(atomicalID) 69 | } 70 | func is_op_return_dmitem_payment_marker_atomical_id(script []byte) string { 71 | if len(script) < 1+5+2+1+36 { // 6a04<01>p 72 | return "" 73 | } 74 | // Ensure it is an OP_RETURN 75 | firstByte := script[0] 76 | secondBytes := script[:2] 77 | if secondBytes[0] != 0x00 && firstByte != 0x6a { 78 | return "" 79 | } 80 | startIndex := 1 81 | if secondBytes[0] == 0x00 { 82 | startIndex = 2 83 | } 84 | // Check for the envelope format 85 | if hex.EncodeToString(script[startIndex:startIndex+5]) != ATOMICALS_ENVELOPE_MARKER_BYTES { 86 | return "" 87 | } 88 | // Check the next op code matches 'p' for payment 89 | if hex.EncodeToString(script[startIndex+5:startIndex+5+2]) != "0164" { 90 | return "" 91 | } 92 | // Check there is a 36 byte push data 93 | if hex.EncodeToString(script[startIndex+5+2:startIndex+5+2+1]) != "24" { 94 | return "" 95 | } 96 | // Extract and return the atomical ID 97 | atomicalID := script[startIndex+5+2+1 : startIndex+5+2+1+36] 98 | return hex.EncodeToString(atomicalID) 99 | } 100 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "github.com/shopspring/decimal" 7 | ) 8 | 9 | func Sha256(data []byte) []byte { 10 | hash := sha256.New() 11 | hash.Write(data) 12 | return hash.Sum(nil) 13 | } 14 | func DoubleSha256(x []byte) []byte { 15 | // '''SHA-256 of SHA-256, as used extensively in bitcoin.''' 16 | return Sha256(Sha256(x)) 17 | } 18 | 19 | func MulSatoshi(value float64) int64 { 20 | decimalResult := decimal.NewFromFloat(value).Mul(Satoshi) 21 | int64Result, _ := decimalResult.Float64() 22 | return int64(int64Result) 23 | } 24 | --------------------------------------------------------------------------------