├── LICENSE ├── README.md ├── cmd └── main.go ├── conf └── conf.go ├── constant └── constant.go ├── data └── brc20.input.txt ├── decimal ├── decimal.go └── decimal_test.go ├── event └── event.go ├── go.mod ├── go.sum ├── indexer ├── brc20_deploy.go ├── brc20_mint.go ├── brc20_transfer.go ├── indexer.go ├── init.go ├── module_approve.go ├── module_commit.go ├── module_commit_add_liq.go ├── module_commit_decrease_approval.go ├── module_commit_deploy_pool.go ├── module_commit_gas.go ├── module_commit_inscribe.go ├── module_commit_remove_liq.go ├── module_commit_send.go ├── module_commit_sendlp.go ├── module_commit_swap.go ├── module_commit_utils.go ├── module_commit_verify.go ├── module_cond_approve.go ├── module_create.go ├── module_withdraw.go └── store_into_data.go ├── loader ├── dump.go ├── load_from_data.go └── load_from_json.go ├── model ├── conditional_approve.go ├── history.go ├── history_module.go ├── model.go ├── module.go ├── store.go └── swap.go └── utils ├── bip322 └── verify.go ├── script ├── compress.go ├── compress_test.go └── opcode.go └── utils.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 UniSat 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 | # UniSat Indexer (brc-20) library 2 | 3 | 4 | This library fully implements the specification protocol of brc-20. 5 | 6 | Developers can integrate this library in the code according to their needs. 7 | 8 | 9 | # Example `cmd/main.go` 10 | 11 | When the `cmd/main.go` sample program is running, it will analyze and index the latest data based on the input data, and output a detailed list of all Token information and holder balances in text form. 12 | 13 | unisat@ordinals:~/brc20/brc20-indexer$ go build ./cmd/main.go 14 | unisat@ordinals:~/brc20/brc20-indexer$ ./main 15 | 2023/05/04 22:17:13 ProcessUpdateLatestBRC20 update. total 10569 16 | 2023/05/04 22:17:13 ProcessUpdateLatestBRC20 finish. ticker: 59, users: 2282, tokens: 59, validTransfer: 3, invalidTransfer: 2 17 | 18 | The example input data is until block height 783025 (2023-03-09 19:19:43 UTC) , the results: 19 | 20 | ... 21 | ordi history: 9356, valid: 9355, minted: 8961661, holders: 2181 22 | ordi bc1q9zzrdj7w0c4f8j6tkwcr8z3c5a6qav9tfgp70s history: 10, transfer: 0, balance: 10000, tokens: 1 23 | ordi bc1qghsuvxj70q0ckh0d8j2m3g9ndjz4lpyehtq4ln history: 1, transfer: 0, balance: 1000, tokens: 4 24 | ordi bc1qf8t5z9j3fhrwnltha7u9lqvvtw4uux02k0600v history: 95, transfer: 0, balance: 95000, tokens: 1 25 | ordi bc1qtncrdztdrygchjgelsyr9prrdr224x3vt9p2hd history: 4, transfer: 0, balance: 4000, tokens: 1 26 | ordi bc1qvgm4ae49cktgpm33p0pr3s3ln7j203xu9z59t7 history: 3, transfer: 0, balance: 3000, tokens: 1 27 | ordi bc1q3j2djzt4j5mr0v9wduhq7p0xzs3fzufzggfma8 history: 101, transfer: 0, balance: 101000, tokens: 1 28 | ordi bc1qe594y2hzlhqgksefxslluza2mm2nz02hpe7vp5 history: 14, transfer: 0, balance: 14000, tokens: 1 29 | ordi bc1qatp97a6pnxutnncrrth39lff5zcreuuscjawf6 history: 3, transfer: 0, balance: 3000, tokens: 1 30 | ordi bc1qa4nt24mpz4evk44xqnjcz9xl66f5enew82ru8y history: 26, transfer: 0, balance: 26000, tokens: 1 31 | ordi bc1ql5vdt2pkcu4hc9uu460l58m6fy78euwgsamf97 history: 11, transfer: 0, balance: 11000, tokens: 2 32 | ordi bc1pqqvxt3h05z0e0prq87kup4kzvnrmhscs6gual9zshg7xrhant2lquvr4uz history: 1, transfer: 0, balance: 1000, tokens: 1 33 | ordi bc1pqq48cd7drp9v4z4gwvtdjhune7gdt384gy39794vzj2d48eqyjqqfm4y9g history: 1, transfer: 0, balance: 1000, tokens: 1 34 | ordi bc1pqqkcju49grmppll9m4s63x4drzyzt65sxtjrjkwmr8d57gzkaxwqwphkz5 history: 1, transfer: 0, balance: 1000, tokens: 1 35 | ... 36 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/btcsuite/btcd/chaincfg" 10 | "github.com/unisat-wallet/libbrc20-indexer/conf" 11 | "github.com/unisat-wallet/libbrc20-indexer/indexer" 12 | "github.com/unisat-wallet/libbrc20-indexer/loader" 13 | ) 14 | 15 | var ( 16 | inputfile string 17 | outputfile string 18 | outputModulefile string 19 | testnet bool 20 | ) 21 | 22 | func init() { 23 | flag.BoolVar(&testnet, "testnet", false, "testnet") 24 | flag.StringVar(&inputfile, "input", "./data/brc20.input.txt", "the filename of input data, default(./data/brc20.input.txt)") 25 | flag.StringVar(&outputfile, "output", "./data/brc20.output.txt", "the filename of output data, default(./data/brc20.output.txt)") 26 | flag.StringVar(&outputModulefile, "output_module", "./data/module.output.txt", "the filename of output data, default(./data/module.output.txt)") 27 | 28 | flag.Parse() 29 | 30 | if testnet { 31 | conf.GlobalNetParams = &chaincfg.TestNet3Params 32 | } 33 | 34 | if ticks := os.Getenv("TICKS_ENABLED"); ticks != "" { 35 | conf.TICKS_ENABLED = ticks 36 | } 37 | 38 | if id := os.Getenv("MODULE_SWAP_SOURCE_INSCRIPTION_ID"); id != "" { 39 | conf.MODULE_SWAP_SOURCE_INSCRIPTION_ID = id 40 | } 41 | 42 | if heightStr := os.Getenv("BRC20_ENABLE_SELF_MINT_HEIGHT"); heightStr != "" { 43 | if h, err := strconv.Atoi(heightStr); err != nil { 44 | conf.ENABLE_SELF_MINT_HEIGHT = uint32(h) 45 | } 46 | } 47 | } 48 | 49 | func main() { 50 | brc20Datas := make(chan interface{}, 10240) 51 | go func() { 52 | if err := loader.LoadBRC20InputData(inputfile, brc20Datas); err != nil { 53 | log.Printf("invalid input, %s", err) 54 | } 55 | close(brc20Datas) 56 | }() 57 | 58 | g := &indexer.BRC20ModuleIndexer{} 59 | g.Init() 60 | g.ProcessUpdateLatestBRC20Loop(brc20Datas, nil) 61 | 62 | loader.DumpTickerInfoMap(outputfile, 63 | g.HistoryData, 64 | g.InscriptionsTickerInfoMap, 65 | g.UserTokensBalanceData, 66 | g.TokenUsersBalanceData, 67 | ) 68 | 69 | loader.DumpModuleInfoMap(outputModulefile, 70 | g.ModulesInfoMap, 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/chaincfg" 5 | ) 6 | 7 | var ( 8 | DEBUG = false 9 | MODULE_SWAP_SOURCE_INSCRIPTION_ID = "d2a30f6131324e06b1366876c8c089d7ad2a9c2b0ea971c5b0dc6198615bda2ei0" 10 | GlobalNetParams = &chaincfg.MainNetParams 11 | TICKS_ENABLED = "" 12 | ENABLE_SELF_MINT_HEIGHT uint32 = 837090 13 | ENABLE_SWAP_WITHDRAW_HEIGHT uint32 = 847090 // fixme: dummy height 14 | ) 15 | -------------------------------------------------------------------------------- /constant/constant.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | const MEMPOOL_HEIGHT = 0x3fffff // 3fffff, 2^22-1 4 | 5 | // brc20 protocal 6 | const ( 7 | BRC20_P = "brc-20" 8 | BRC20_P_MODULE = "brc20-module" 9 | BRC20_P_SWAP = "brc20-swap" 10 | ) 11 | 12 | // brc20 op 13 | const ( 14 | BRC20_OP_DEPLOY = "deploy" 15 | BRC20_OP_MINT = "mint" 16 | BRC20_OP_TRANSFER = "transfer" 17 | ) 18 | 19 | // brc20 history 20 | const ( 21 | BRC20_HISTORY_TYPE_INSCRIBE_DEPLOY = "inscribe-deploy" 22 | BRC20_HISTORY_TYPE_INSCRIBE_MINT = "inscribe-mint" 23 | BRC20_HISTORY_TYPE_INSCRIBE_TRANSFER = "inscribe-transfer" 24 | BRC20_HISTORY_TYPE_TRANSFER = "transfer" 25 | BRC20_HISTORY_TYPE_SEND = "send" 26 | BRC20_HISTORY_TYPE_RECEIVE = "receive" 27 | ) 28 | 29 | const ( 30 | // brc20 history N 31 | BRC20_HISTORY_TYPE_N_INSCRIBE_DEPLOY uint8 = 0 32 | BRC20_HISTORY_TYPE_N_INSCRIBE_MINT uint8 = 1 33 | BRC20_HISTORY_TYPE_N_INSCRIBE_TRANSFER uint8 = 2 34 | BRC20_HISTORY_TYPE_N_TRANSFER uint8 = 3 35 | BRC20_HISTORY_TYPE_N_SEND uint8 = 4 36 | BRC20_HISTORY_TYPE_N_RECEIVE uint8 = 5 37 | 38 | // swap history N 39 | BRC20_HISTORY_MODULE_TYPE_N_INSCRIBE_MODULE uint8 = 6 40 | 41 | BRC20_HISTORY_MODULE_TYPE_N_INSCRIBE_WITHDRAW uint8 = 7 42 | BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW uint8 = 8 43 | BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_FROM uint8 = 9 44 | BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_TO uint8 = 10 45 | 46 | BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_APPROVE uint8 = 11 47 | BRC20_HISTORY_SWAP_TYPE_N_APPROVE uint8 = 12 48 | 49 | BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_CONDITIONAL_APPROVE uint8 = 13 50 | BRC20_HISTORY_SWAP_TYPE_N_CONDITIONAL_APPROVE uint8 = 14 51 | 52 | BRC20_HISTORY_SWAP_TYPE_N_APPROVE_FROM uint8 = 15 53 | BRC20_HISTORY_SWAP_TYPE_N_APPROVE_TO uint8 = 16 54 | 55 | BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_COMMIT uint8 = 17 56 | BRC20_HISTORY_SWAP_TYPE_N_COMMIT uint8 = 18 57 | ) 58 | 59 | // module op 60 | const ( 61 | BRC20_OP_MODULE_DEPLOY = "deploy" 62 | BRC20_OP_MODULE_WITHDRAW = "withdraw" 63 | BRC20_OP_SWAP_APPROVE = "approve" 64 | BRC20_OP_SWAP_CONDITIONAL_APPROVE = "conditional-approve" 65 | BRC20_OP_SWAP_COMMIT = "commit" 66 | ) 67 | 68 | // swap history 69 | const ( 70 | BRC20_HISTORY_MODULE_TYPE_INSCRIBE_MODULE = "inscribe-module" 71 | BRC20_HISTORY_MODULE_TYPE_INSCRIBE_WITHDRAW = "inscribe-withdraw" 72 | BRC20_HISTORY_MODULE_TYPE_WITHDRAW_FROM = "withdraw-from" 73 | BRC20_HISTORY_MODULE_TYPE_WITHDRAW_TO = "withdraw-to" 74 | 75 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_APPROVE = "inscribe-approve" 76 | BRC20_HISTORY_SWAP_TYPE_APPROVE = "approve" 77 | 78 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_CONDITIONAL_APPROVE = "inscribe-conditional-approve" 79 | BRC20_HISTORY_SWAP_TYPE_CONDITIONAL_APPROVE = "conditional-approve" 80 | 81 | BRC20_HISTORY_SWAP_TYPE_APPROVE_FROM = "approve-from" 82 | BRC20_HISTORY_SWAP_TYPE_APPROVE_TO = "approve-to" 83 | 84 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_COMMIT = "inscribe-commit" 85 | BRC20_HISTORY_SWAP_TYPE_COMMIT = "commit" 86 | ) 87 | 88 | var BRC20_HISTORY_TYPE_NAMES []string = []string{ 89 | BRC20_HISTORY_TYPE_INSCRIBE_DEPLOY, 90 | BRC20_HISTORY_TYPE_INSCRIBE_MINT, 91 | BRC20_HISTORY_TYPE_INSCRIBE_TRANSFER, 92 | BRC20_HISTORY_TYPE_TRANSFER, 93 | BRC20_HISTORY_TYPE_SEND, 94 | BRC20_HISTORY_TYPE_RECEIVE, 95 | 96 | // module 97 | BRC20_HISTORY_MODULE_TYPE_INSCRIBE_MODULE, 98 | BRC20_HISTORY_MODULE_TYPE_INSCRIBE_WITHDRAW, 99 | BRC20_HISTORY_MODULE_TYPE_WITHDRAW_FROM, 100 | BRC20_HISTORY_MODULE_TYPE_WITHDRAW_TO, 101 | 102 | // swap 103 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_APPROVE, 104 | BRC20_HISTORY_SWAP_TYPE_APPROVE, 105 | 106 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_CONDITIONAL_APPROVE, 107 | BRC20_HISTORY_SWAP_TYPE_CONDITIONAL_APPROVE, 108 | 109 | BRC20_HISTORY_SWAP_TYPE_APPROVE_FROM, 110 | BRC20_HISTORY_SWAP_TYPE_APPROVE_TO, 111 | 112 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_COMMIT, 113 | BRC20_HISTORY_SWAP_TYPE_COMMIT, 114 | } 115 | 116 | var BRC20_HISTORY_TYPES_TO_N map[string]uint8 = map[string]uint8{ 117 | BRC20_HISTORY_TYPE_INSCRIBE_DEPLOY: BRC20_HISTORY_TYPE_N_INSCRIBE_DEPLOY, 118 | BRC20_HISTORY_TYPE_INSCRIBE_MINT: BRC20_HISTORY_TYPE_N_INSCRIBE_MINT, 119 | BRC20_HISTORY_TYPE_INSCRIBE_TRANSFER: BRC20_HISTORY_TYPE_N_INSCRIBE_TRANSFER, 120 | BRC20_HISTORY_TYPE_TRANSFER: BRC20_HISTORY_TYPE_N_TRANSFER, 121 | BRC20_HISTORY_TYPE_SEND: BRC20_HISTORY_TYPE_N_SEND, 122 | BRC20_HISTORY_TYPE_RECEIVE: BRC20_HISTORY_TYPE_N_RECEIVE, 123 | 124 | // module 125 | BRC20_HISTORY_MODULE_TYPE_INSCRIBE_MODULE: BRC20_HISTORY_MODULE_TYPE_N_INSCRIBE_MODULE, 126 | BRC20_HISTORY_MODULE_TYPE_INSCRIBE_WITHDRAW: BRC20_HISTORY_MODULE_TYPE_N_INSCRIBE_WITHDRAW, 127 | BRC20_HISTORY_MODULE_TYPE_WITHDRAW_FROM: BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_FROM, 128 | BRC20_HISTORY_MODULE_TYPE_WITHDRAW_TO: BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_TO, 129 | 130 | // swap // swap 131 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_APPROVE: BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_APPROVE, 132 | BRC20_HISTORY_SWAP_TYPE_APPROVE: BRC20_HISTORY_SWAP_TYPE_N_APPROVE, 133 | 134 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_CONDITIONAL_APPROVE: BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_CONDITIONAL_APPROVE, 135 | BRC20_HISTORY_SWAP_TYPE_CONDITIONAL_APPROVE: BRC20_HISTORY_SWAP_TYPE_N_CONDITIONAL_APPROVE, 136 | 137 | BRC20_HISTORY_SWAP_TYPE_APPROVE_FROM: BRC20_HISTORY_SWAP_TYPE_N_APPROVE_FROM, 138 | BRC20_HISTORY_SWAP_TYPE_APPROVE_TO: BRC20_HISTORY_SWAP_TYPE_N_APPROVE_TO, 139 | 140 | BRC20_HISTORY_SWAP_TYPE_INSCRIBE_COMMIT: BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_COMMIT, 141 | BRC20_HISTORY_SWAP_TYPE_COMMIT: BRC20_HISTORY_SWAP_TYPE_N_COMMIT, 142 | } 143 | 144 | // swap function 145 | const ( 146 | BRC20_SWAP_FUNCTION_DEPLOY_POOL = "deployPool" 147 | BRC20_SWAP_FUNCTION_ADD_LIQ = "addLiq" 148 | BRC20_SWAP_FUNCTION_REMOVE_LIQ = "removeLiq" 149 | BRC20_SWAP_FUNCTION_SWAP = "swap" 150 | BRC20_SWAP_FUNCTION_SEND = "send" 151 | BRC20_SWAP_FUNCTION_SENDLP = "sendLp" 152 | BRC20_SWAP_FUNCTION_DECREASE_APPROVAL = "decreaseApproval" 153 | ) 154 | 155 | const ZERO_ADDRESS_PKSCRIPT = "\x6a\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 156 | -------------------------------------------------------------------------------- /decimal/decimal.go: -------------------------------------------------------------------------------- 1 | package decimal 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/big" 8 | "strings" 9 | ) 10 | 11 | const MAX_PRECISION = 18 12 | 13 | var MAX_PRECISION_STRING = "18" 14 | 15 | var precisionFactor [19]*big.Int = [19]*big.Int{ 16 | new(big.Int).Exp(big.NewInt(10), big.NewInt(0), nil), 17 | new(big.Int).Exp(big.NewInt(10), big.NewInt(1), nil), 18 | new(big.Int).Exp(big.NewInt(10), big.NewInt(2), nil), 19 | new(big.Int).Exp(big.NewInt(10), big.NewInt(3), nil), 20 | new(big.Int).Exp(big.NewInt(10), big.NewInt(4), nil), 21 | new(big.Int).Exp(big.NewInt(10), big.NewInt(5), nil), 22 | new(big.Int).Exp(big.NewInt(10), big.NewInt(6), nil), 23 | new(big.Int).Exp(big.NewInt(10), big.NewInt(7), nil), 24 | new(big.Int).Exp(big.NewInt(10), big.NewInt(8), nil), 25 | new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil), 26 | new(big.Int).Exp(big.NewInt(10), big.NewInt(10), nil), 27 | new(big.Int).Exp(big.NewInt(10), big.NewInt(11), nil), 28 | new(big.Int).Exp(big.NewInt(10), big.NewInt(12), nil), 29 | new(big.Int).Exp(big.NewInt(10), big.NewInt(13), nil), 30 | new(big.Int).Exp(big.NewInt(10), big.NewInt(14), nil), 31 | new(big.Int).Exp(big.NewInt(10), big.NewInt(15), nil), 32 | new(big.Int).Exp(big.NewInt(10), big.NewInt(16), nil), 33 | new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil), 34 | new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil), 35 | } 36 | 37 | // Decimal represents a fixed-point decimal number with 18 decimal places 38 | type Decimal struct { 39 | Precition uint 40 | Value *big.Int 41 | } 42 | 43 | func NewDecimal(v uint64, p uint) *Decimal { 44 | if p > MAX_PRECISION { 45 | p = MAX_PRECISION 46 | } 47 | return &Decimal{Precition: p, Value: new(big.Int).SetUint64(v)} 48 | } 49 | 50 | func NewDecimalCopy(other *Decimal) *Decimal { 51 | if other == nil { 52 | return nil 53 | } 54 | return &Decimal{Precition: other.Precition, Value: new(big.Int).Set(other.Value)} 55 | } 56 | 57 | // NewDecimalFromString creates a Decimal instance from a string 58 | func NewDecimalFromString(s string, maxPrecision int) (*Decimal, error) { 59 | if s == "" { 60 | return nil, errors.New("empty string") 61 | } 62 | 63 | parts := strings.Split(s, ".") 64 | if len(parts) > 2 { 65 | return nil, fmt.Errorf("invalid decimal format: %s", s) 66 | } 67 | 68 | integerPartStr := parts[0] 69 | if integerPartStr == "" || integerPartStr[0] == '+' { 70 | return nil, errors.New("empty integer") 71 | } 72 | 73 | integerPart, ok := new(big.Int).SetString(parts[0], 10) 74 | if !ok { 75 | return nil, fmt.Errorf("invalid integer format: %s", parts[0]) 76 | } 77 | 78 | currPrecision := 0 79 | decimalPart := big.NewInt(0) 80 | if len(parts) == 2 { 81 | decimalPartStr := parts[1] 82 | if decimalPartStr == "" || decimalPartStr[0] == '-' || decimalPartStr[0] == '+' { 83 | return nil, errors.New("empty decimal") 84 | } 85 | 86 | currPrecision = len(decimalPartStr) 87 | if currPrecision > maxPrecision { 88 | return nil, fmt.Errorf("decimal exceeds maximum precision: %s", s) 89 | } 90 | n := maxPrecision - currPrecision 91 | for i := 0; i < n; i++ { 92 | decimalPartStr += "0" 93 | } 94 | decimalPart, ok = new(big.Int).SetString(decimalPartStr, 10) 95 | if !ok || decimalPart.Sign() < 0 { 96 | return nil, fmt.Errorf("invalid decimal format: %s", parts[0]) 97 | } 98 | } 99 | 100 | value := new(big.Int).Mul(integerPart, precisionFactor[maxPrecision]) 101 | if value.Sign() < 0 { 102 | value = value.Sub(value, decimalPart) 103 | } else { 104 | value = value.Add(value, decimalPart) 105 | } 106 | 107 | return &Decimal{Precition: uint(maxPrecision), Value: value}, nil 108 | } 109 | 110 | // String returns the string representation of a Decimal instance 111 | func (d *Decimal) String() string { 112 | if d == nil { 113 | return "0" 114 | } 115 | value := new(big.Int).Abs(d.Value) 116 | quotient, remainder := new(big.Int).QuoRem(value, precisionFactor[d.Precition], new(big.Int)) 117 | sign := "" 118 | if d.Value.Sign() < 0 { 119 | sign = "-" 120 | } 121 | if remainder.Sign() == 0 { 122 | return fmt.Sprintf("%s%s", sign, quotient.String()) 123 | } 124 | decimalPart := fmt.Sprintf("%0*d", d.Precition, remainder) 125 | decimalPart = strings.TrimRight(decimalPart, "0") 126 | return fmt.Sprintf("%s%s.%s", sign, quotient.String(), decimalPart) 127 | } 128 | 129 | // Add adds two Decimal instances and returns a new Decimal instance 130 | func (d *Decimal) Add(other *Decimal) *Decimal { 131 | if d == nil && other == nil { 132 | return nil 133 | } 134 | if other == nil { 135 | value := new(big.Int).Set(d.Value) 136 | return &Decimal{Precition: d.Precition, Value: value} 137 | } 138 | if d == nil { 139 | value := new(big.Int).Set(other.Value) 140 | return &Decimal{Precition: other.Precition, Value: value} 141 | } 142 | if d.Precition != other.Precition { 143 | panic("precition not match") 144 | } 145 | value := new(big.Int).Add(d.Value, other.Value) 146 | return &Decimal{Precition: d.Precition, Value: value} 147 | } 148 | 149 | // Sub subtracts two Decimal instances and returns a new Decimal instance 150 | func (d *Decimal) Sub(other *Decimal) *Decimal { 151 | if d == nil && other == nil { 152 | return nil 153 | } 154 | if other == nil { 155 | value := new(big.Int).Set(d.Value) 156 | return &Decimal{Precition: d.Precition, Value: value} 157 | } 158 | if d == nil { 159 | value := new(big.Int).Neg(other.Value) 160 | return &Decimal{Precition: other.Precition, Value: value} 161 | } 162 | if d.Precition != other.Precition { 163 | panic(fmt.Sprintf("precition not match, (%d != %d)", d.Precition, other.Precition)) 164 | } 165 | value := new(big.Int).Sub(d.Value, other.Value) 166 | return &Decimal{Precition: d.Precition, Value: value} 167 | } 168 | 169 | // Mul muls two Decimal instances and returns a new Decimal instance 170 | func (d *Decimal) Mul(other *Decimal) *Decimal { 171 | if d == nil || other == nil { 172 | return nil 173 | } 174 | value := new(big.Int).Mul(d.Value, other.Value) 175 | // value := new(big.Int).Div(value0, precisionFactor[other.Precition]) 176 | return &Decimal{Precition: d.Precition, Value: value} 177 | } 178 | 179 | // Sqrt muls two Decimal instances and returns a new Decimal instance 180 | func (d *Decimal) Sqrt() *Decimal { 181 | if d == nil { 182 | return nil 183 | } 184 | // value0 := new(big.Int).Mul(d.Value, precisionFactor[d.Precition]) 185 | value := new(big.Int).Sqrt(d.Value) 186 | return &Decimal{Precition: MAX_PRECISION, Value: value} 187 | } 188 | 189 | // Div divs two Decimal instances and returns a new Decimal instance 190 | func (d *Decimal) Div(other *Decimal) *Decimal { 191 | if d == nil || other == nil { 192 | return nil 193 | } 194 | // value0 := new(big.Int).Mul(d.Value, precisionFactor[other.Precition]) 195 | value := new(big.Int).Div(d.Value, other.Value) 196 | return &Decimal{Precition: d.Precition, Value: value} 197 | } 198 | 199 | func (d *Decimal) Cmp(other *Decimal) int { 200 | if d == nil && other == nil { 201 | return 0 202 | } 203 | if other == nil { 204 | return d.Value.Sign() 205 | } 206 | if d == nil { 207 | return -other.Value.Sign() 208 | } 209 | if d.Precition != other.Precition { 210 | panic(fmt.Sprintf("precition not match, (%d != %d)", d.Precition, other.Precition)) 211 | } 212 | return d.Value.Cmp(other.Value) 213 | } 214 | 215 | func (d *Decimal) CmpAlign(other *Decimal) int { 216 | if d == nil && other == nil { 217 | return 0 218 | } 219 | if other == nil { 220 | return d.Value.Sign() 221 | } 222 | if d == nil { 223 | return -other.Value.Sign() 224 | } 225 | return d.Value.Cmp(other.Value) 226 | } 227 | 228 | func (d *Decimal) Sign() int { 229 | if d == nil { 230 | return 0 231 | } 232 | return d.Value.Sign() 233 | } 234 | 235 | func (d *Decimal) IsOverflowUint64() bool { 236 | if d == nil { 237 | return false 238 | } 239 | 240 | integerPart := new(big.Int).SetUint64(math.MaxUint64) 241 | value := new(big.Int).Mul(integerPart, precisionFactor[d.Precition]) 242 | if d.Value.Cmp(value) > 0 { 243 | return true 244 | } 245 | return false 246 | } 247 | 248 | func (d *Decimal) GetMaxUint64() *Decimal { 249 | if d == nil { 250 | return nil 251 | } 252 | integerPart := new(big.Int).SetUint64(math.MaxUint64) 253 | value := new(big.Int).Mul(integerPart, precisionFactor[d.Precition]) 254 | return &Decimal{Precition: d.Precition, Value: value} 255 | } 256 | 257 | func (d *Decimal) Float64() float64 { 258 | if d == nil { 259 | return 0 260 | } 261 | value := new(big.Int).Abs(d.Value) 262 | quotient, remainder := new(big.Int).QuoRem(value, precisionFactor[d.Precition], new(big.Int)) 263 | f := float64(quotient.Uint64()) + float64(remainder.Uint64())/math.MaxFloat64 264 | if d.Value.Sign() < 0 { 265 | return -f 266 | } 267 | return f 268 | } 269 | -------------------------------------------------------------------------------- /decimal/decimal_test.go: -------------------------------------------------------------------------------- 1 | package decimal_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 7 | ) 8 | 9 | func TestNewDecimalFromString(t *testing.T) { 10 | testCases := []struct { 11 | input string 12 | want string 13 | err bool 14 | }{ 15 | // valid 16 | {"123456789.123456789", "123456789.123456789", false}, 17 | {"123456789.123", "123456789.123", false}, 18 | {"123456789", "123456789", false}, 19 | {"-123456789.123456789", "-123456789.123456789", false}, 20 | {"-123456789.123", "-123456789.123", false}, 21 | {"-123456789", "-123456789", false}, 22 | {"000001", "1", false}, 23 | {"000001.1", "1.1", false}, 24 | {"000001.100000000000000000", "1.1", false}, 25 | 26 | // invalid 27 | {"", "", true}, 28 | {" ", "", true}, 29 | {".", "", true}, 30 | {" 123.456", "", true}, 31 | {".456", "", true}, 32 | {".456 ", "", true}, 33 | {" .456 ", "", true}, 34 | {" 456", "", true}, 35 | {"456 ", "", true}, 36 | {"45 6", "", true}, 37 | {"123. 456", "", true}, 38 | {"123.-456", "", true}, 39 | {"123.+456", "", true}, 40 | {"+123.456", "", true}, 41 | {"123.456.789", "", true}, 42 | {"123456789.", "123456789", true}, 43 | {"123456789.12345678901234567891", "", true}, 44 | {"0.1000000000000000000", "", true}, 45 | } 46 | 47 | for _, tc := range testCases { 48 | t.Run(tc.input, func(t *testing.T) { 49 | got, err := decimal.NewDecimalFromString(tc.input, 18) 50 | if (err != nil) != tc.err { 51 | t.Fatalf("unexpected error: %v", err) 52 | } 53 | if err == nil && got.String() != tc.want { 54 | t.Errorf("got %s, want %s", got.String(), tc.want) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestDecimal_Add(t *testing.T) { 61 | testCases := []struct { 62 | a, b string 63 | want string 64 | }{ 65 | {"123456789.123456789", "987654321.987654321", "1111111111.11111111"}, 66 | {"123456789.123", "987654321.987", "1111111111.11"}, 67 | {"123456789", "987654321", "1111111110"}, 68 | {"-123456789.123456789", "987654321.987654321", "864197532.864197532"}, 69 | {"-123456789.123", "987654321.987", "864197532.864"}, 70 | {"-123456789", "987654321", "864197532"}, 71 | } 72 | 73 | for _, tc := range testCases { 74 | t.Run(tc.a+"+"+tc.b, func(t *testing.T) { 75 | da, _ := decimal.NewDecimalFromString(tc.a, 18) 76 | db, _ := decimal.NewDecimalFromString(tc.b, 18) 77 | got := da.Add(db) 78 | if got.String() != tc.want { 79 | t.Errorf("got %s, want %s", got.String(), tc.want) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestDecimal_Sub(t *testing.T) { 86 | testCases := []struct { 87 | a, b string 88 | want string 89 | }{ 90 | {"123456789.123456789", "987654321.987654321", "-864197532.864197532"}, 91 | {"123456789.123", "987654321.987", "-864197532.864"}, 92 | {"123456789", "987654321", "-864197532"}, 93 | {"-123456789.123456789", "987654321.987654321", "-1111111111.11111111"}, 94 | {"-123456789.123", "987654321.987", "-1111111111.11"}, 95 | {"-123456789", "987654321", "-1111111110"}, 96 | } 97 | 98 | for _, tc := range testCases { 99 | t.Run(tc.a+"-"+tc.b, func(t *testing.T) { 100 | da, _ := decimal.NewDecimalFromString(tc.a, 18) 101 | db, _ := decimal.NewDecimalFromString(tc.b, 18) 102 | got := da.Sub(db) 103 | if got.String() != tc.want { 104 | t.Errorf("got %s, want %s", got.String(), tc.want) 105 | } 106 | }) 107 | } 108 | 109 | } 110 | 111 | func TestDecimal_String(t *testing.T) { 112 | testCases := []struct { 113 | input string 114 | want string 115 | }{ 116 | {"123456789.123456789", "123456789.123456789"}, 117 | {"123456789", "123456789"}, 118 | {"-987654321.987654321", "-987654321.987654321"}, 119 | {"0.123456789", "0.123456789"}, 120 | {"0.123", "0.123"}, 121 | {"123456789", "123456789"}, 122 | {"-123456789", "-123456789"}, 123 | } 124 | 125 | for _, tc := range testCases { 126 | t.Run(tc.input, func(t *testing.T) { 127 | d, _ := decimal.NewDecimalFromString(tc.input, 18) 128 | got := d.String() 129 | if got != tc.want { 130 | t.Errorf("got %s, want %s", got, tc.want) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func BenchmarkAdd(b *testing.B) { 137 | d1, _ := decimal.NewDecimalFromString("123456789.123456789", 18) 138 | d2, _ := decimal.NewDecimalFromString("987654321.987654321", 18) 139 | for n := 0; n < b.N; n++ { 140 | d1.Add(d2) 141 | } 142 | } 143 | 144 | func BenchmarkSub(b *testing.B) { 145 | d1, _ := decimal.NewDecimalFromString("123456789.123456789", 18) 146 | d2, _ := decimal.NewDecimalFromString("987654321.987654321", 18) 147 | for n := 0; n < b.N; n++ { 148 | d1.Sub(d2) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/unisat-wallet/libbrc20-indexer/conf" 14 | "github.com/unisat-wallet/libbrc20-indexer/constant" 15 | "github.com/unisat-wallet/libbrc20-indexer/model" 16 | "github.com/unisat-wallet/libbrc20-indexer/utils" 17 | ) 18 | 19 | func InitTickDataFromFile(fname string) (brc20Datas []*model.InscriptionBRC20Data, err error) { 20 | // Open our jsonFile 21 | jsonFile, err := os.Open(fname) 22 | // if we os.Open returns an error then handle it 23 | if err != nil { 24 | fmt.Println(err) 25 | return nil, err 26 | } 27 | // defer the closing of our jsonFile so that we can parse it later on 28 | defer jsonFile.Close() 29 | 30 | byteValue, err := ioutil.ReadAll(jsonFile) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | var ticksExternal []*model.InscriptionBRC20DeployContent 36 | err = json.Unmarshal([]byte(byteValue), &ticksExternal) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | for idx, info := range ticksExternal { 42 | var data model.InscriptionBRC20Data 43 | 44 | data.TxId = fmt.Sprintf("10%030x", idx) 45 | data.Idx = 0 46 | data.Vout = 0 47 | data.Offset = 0 48 | data.Satoshi = 330 49 | 50 | data.InscriptionId = fmt.Sprintf("10%030xi0", idx) 51 | data.InscriptionNumber = 100 + int64(idx) 52 | 53 | data.Height = 1 54 | data.TxIdx = uint32(idx) 55 | data.BlockTime = 100 56 | var key model.NFTCreateIdxKey = model.NFTCreateIdxKey{ 57 | Height: data.Height, 58 | IdxInBlock: uint64(idx), // fake idx 59 | } 60 | data.CreateIdxKey = key.String() 61 | data.IsTransfer = false 62 | 63 | data.ContentBody, _ = json.Marshal(info) 64 | data.Sequence = 0 65 | 66 | brc20Datas = append(brc20Datas, &data) 67 | } 68 | return brc20Datas, nil 69 | } 70 | 71 | func GenerateBRC20InputDataFromEvents(fname string) (brc20Datas []*model.InscriptionBRC20Data, err error) { 72 | // Open our jsonFile 73 | jsonFile, err := os.Open(fname) 74 | // if we os.Open returns an error then handle it 75 | if err != nil { 76 | fmt.Println(err) 77 | return nil, err 78 | } 79 | // defer the closing of our jsonFile so that we can parse it later on 80 | defer jsonFile.Close() 81 | 82 | byteValue, err := ioutil.ReadAll(jsonFile) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | var events []*model.BRC20ModuleHistoryInfoEvent 88 | err = json.Unmarshal([]byte(byteValue), &events) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | for idx, e := range events { 94 | var data model.InscriptionBRC20Data 95 | 96 | txid, _ := hex.DecodeString(e.TxIdHex) 97 | data.TxId = string(txid) 98 | 99 | data.Idx = e.Idx 100 | data.Vout = e.Vout 101 | data.Offset = e.Offset 102 | data.Satoshi = e.Satoshi 103 | 104 | data.InscriptionId = e.InscriptionId // preset cache 105 | data.InscriptionNumber = e.InscriptionNumber 106 | 107 | data.Height = e.Height 108 | data.TxIdx = e.TxIdx 109 | data.BlockTime = e.BlockTime 110 | 111 | var key model.NFTCreateIdxKey = model.NFTCreateIdxKey{ 112 | Height: data.Height, 113 | IdxInBlock: uint64(idx), // fake idx 114 | } 115 | data.CreateIdxKey = key.String() 116 | 117 | var pkScriptFrom, pkScriptTo string 118 | if pk, err := utils.GetPkScriptByAddress(e.AddressFrom, conf.GlobalNetParams); err != nil { 119 | log.Printf("GenerateBRC20InputDataFromEvents [%d] pk invalid: %s", idx, err) 120 | } else { 121 | pkScriptFrom = string(pk) 122 | } 123 | if pk, err := utils.GetPkScriptByAddress(e.AddressTo, conf.GlobalNetParams); err != nil { 124 | pk, _ := hex.DecodeString(e.AddressTo) 125 | pkScriptTo = string(pk) 126 | } else { 127 | pkScriptTo = string(pk) 128 | } 129 | 130 | data.PkScript = pkScriptTo 131 | 132 | var inscribe model.InscriptionBRC20Data 133 | inscribe = data 134 | inscribe.IsTransfer = false 135 | 136 | inscribe.ContentBody = []byte(e.ContentBody) 137 | inscribe.Sequence = 0 138 | 139 | if e.Type == "transfer" { 140 | // mint 141 | var mint model.InscriptionBRC20Data 142 | mint = data 143 | mint.ContentBody = []byte(strings.Replace(e.ContentBody, "transfer", "mint", 1)) 144 | mint.Sequence = 0 145 | mint.PkScript = pkScriptFrom 146 | brc20Datas = append(brc20Datas, &mint) 147 | 148 | // transfer 149 | inscribe.PkScript = pkScriptFrom 150 | brc20Datas = append(brc20Datas, &inscribe) 151 | 152 | // transfer send 153 | data.IsTransfer = true 154 | data.Sequence = 1 155 | data.PkScript = pkScriptTo 156 | brc20Datas = append(brc20Datas, &data) 157 | 158 | } else if e.Type == "commit" { 159 | inscribe.PkScript = pkScriptFrom 160 | 161 | brc20Datas = append(brc20Datas, &inscribe) 162 | 163 | // commit send 164 | data.IsTransfer = true 165 | data.Sequence = 1 166 | brc20Datas = append(brc20Datas, &data) 167 | 168 | } else if e.Type == "inscribe-module" { 169 | inscribe.PkScript = pkScriptTo 170 | 171 | brc20Datas = append(brc20Datas, &inscribe) 172 | 173 | } else if e.Type == "inscribe-conditional-approve" { 174 | inscribe.PkScript = pkScriptTo 175 | brc20Datas = append(brc20Datas, &inscribe) 176 | 177 | // send to delegator 178 | data.IsTransfer = true 179 | data.Sequence = 1 180 | data.PkScript = constant.ZERO_ADDRESS_PKSCRIPT 181 | brc20Datas = append(brc20Datas, &data) 182 | 183 | } else if e.Type == "conditional-approve" { 184 | content := strings.Replace(e.ContentBody, "brc20-swap", "brc-20", 1) 185 | 186 | // fixme: always use mint may fail 187 | // mint 188 | var mint model.InscriptionBRC20Data 189 | mint = data 190 | 191 | // e.Data.Amount fixme: replace e.Data.Amount with 192 | 193 | m1 := regexp.MustCompile(`"amt" *: *"[0-9\.]+"`) 194 | content = m1.ReplaceAllString(content, fmt.Sprintf(`"amt":"%s"`, e.Data.Amount)) 195 | 196 | mintContent := strings.Replace(content, "conditional-approve", "mint", 1) 197 | mint.ContentBody = []byte(mintContent) 198 | mint.IsTransfer = false 199 | mint.Sequence = 0 200 | mint.PkScript = pkScriptTo 201 | brc20Datas = append(brc20Datas, &mint) 202 | 203 | // transfer 204 | var transfer model.InscriptionBRC20Data 205 | transfer = data 206 | transferContent := strings.Replace(content, "conditional-approve", "transfer", 1) 207 | transfer.ContentBody = []byte(transferContent) 208 | transfer.IsTransfer = false 209 | transfer.Sequence = 0 210 | transfer.PkScript = pkScriptTo 211 | brc20Datas = append(brc20Datas, &transfer) 212 | 213 | // transfer send 214 | var send model.InscriptionBRC20Data 215 | send = data 216 | send.IsTransfer = true 217 | send.Sequence = 1 218 | send.PkScript = pkScriptFrom 219 | brc20Datas = append(brc20Datas, &send) 220 | 221 | // send to delegator 222 | data.IsTransfer = true 223 | data.Sequence = 2 224 | data.PkScript = constant.ZERO_ADDRESS_PKSCRIPT 225 | brc20Datas = append(brc20Datas, &data) 226 | 227 | } else { 228 | log.Printf("GenerateBRC20InputDataFromEvents [%d] op invalid: %s", idx, e.Type) 229 | // fixme: inscribe-conditional-approve / conditional-approve 230 | } 231 | 232 | } 233 | 234 | return brc20Datas, nil 235 | } 236 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unisat-wallet/libbrc20-indexer 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/btcsuite/btcd v0.23.4 7 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 8 | github.com/btcsuite/btcd/btcutil v1.1.0 9 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 10 | ) 11 | 12 | require ( 13 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 14 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect 15 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 16 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 17 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 2 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 3 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 4 | github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= 5 | github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= 6 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 7 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= 8 | github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 9 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 10 | github.com/btcsuite/btcd/btcutil v1.1.0 h1:MO4klnGY+EWJdoWF12Wkuf4AWDBPMpZNeN/jRLrklUU= 11 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 12 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 13 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 14 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= 15 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 16 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 17 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 18 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 19 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 20 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 21 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 22 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 23 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 24 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 28 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 29 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 30 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 31 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 32 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 33 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 34 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 36 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 37 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 38 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 39 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 40 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 41 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 43 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 45 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 46 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 47 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 48 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 49 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 50 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 51 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 52 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 53 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 54 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 55 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 56 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 57 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 59 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 60 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 61 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 62 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 63 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 64 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 65 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 66 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 67 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 68 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 69 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= 79 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 81 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 84 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 87 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 88 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 89 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 90 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 91 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 94 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 95 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 99 | -------------------------------------------------------------------------------- /indexer/brc20_deploy.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/unisat-wallet/libbrc20-indexer/conf" 10 | "github.com/unisat-wallet/libbrc20-indexer/constant" 11 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 12 | "github.com/unisat-wallet/libbrc20-indexer/model" 13 | "github.com/unisat-wallet/libbrc20-indexer/utils" 14 | ) 15 | 16 | func (g *BRC20ModuleIndexer) ProcessDeploy(data *model.InscriptionBRC20Data) error { 17 | body := new(model.InscriptionBRC20DeployContent) 18 | if err := body.Unmarshal(data.ContentBody); err != nil { 19 | return nil 20 | } 21 | 22 | // check tick 23 | uniqueLowerTicker, err := utils.GetValidUniqueLowerTickerTicker(body.BRC20Tick) 24 | if err != nil { 25 | return nil 26 | // return errors.New("deploy, tick length not 4 or 5") 27 | } 28 | 29 | if len(body.BRC20Tick) == 5 { 30 | if body.BRC20SelfMint != "true" { 31 | return nil 32 | // return errors.New("deploy, tick length 5, but not self_mint") 33 | } 34 | if data.Height < conf.ENABLE_SELF_MINT_HEIGHT { 35 | return nil 36 | // return errors.New("deploy, tick length 5, but not enabled") 37 | } 38 | } 39 | 40 | // tick enable, fixme: test only, not support space in ticker 41 | if conf.TICKS_ENABLED != "" { 42 | if strings.Contains(uniqueLowerTicker, " ") { 43 | return nil 44 | } 45 | if !strings.Contains(conf.TICKS_ENABLED, uniqueLowerTicker) { 46 | return nil 47 | } 48 | } 49 | 50 | if _, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker]; ok { // dup ticker 51 | return nil 52 | // return errors.New("deploy, but tick exist") 53 | } 54 | if body.BRC20Max == "" { // without max 55 | log.Printf("deploy, but max missing. ticker: %s", 56 | uniqueLowerTicker, 57 | ) 58 | return errors.New("deploy, but max missing") 59 | } 60 | 61 | tinfo := model.NewInscriptionBRC20TickInfo(body.BRC20Tick, body.Operation, data) 62 | tinfo.Data.BRC20Max = body.BRC20Max 63 | tinfo.Data.BRC20Limit = body.BRC20Limit 64 | tinfo.Data.BRC20Decimal = body.BRC20Decimal 65 | tinfo.Data.BRC20Minted = "0" 66 | tinfo.InscriptionNumberStart = data.InscriptionNumber 67 | 68 | if len(body.BRC20Tick) == 5 && body.BRC20SelfMint == "true" { 69 | tinfo.SelfMint = true 70 | tinfo.Data.BRC20SelfMint = "true" 71 | } 72 | 73 | // dec 74 | if dec, err := strconv.ParseUint(tinfo.Data.BRC20Decimal, 10, 64); err != nil || dec > 18 { 75 | // dec invalid 76 | log.Printf("deploy, but dec invalid. ticker: %s, dec: %s", 77 | uniqueLowerTicker, 78 | tinfo.Data.BRC20Decimal, 79 | ) 80 | return errors.New("deploy, but dec invalid") 81 | } else { 82 | tinfo.Decimal = uint8(dec) 83 | } 84 | 85 | // max 86 | if max, err := decimal.NewDecimalFromString(body.BRC20Max, int(tinfo.Decimal)); err != nil { 87 | // max invalid 88 | log.Printf("deploy, but max invalid. ticker: %s, max: '%s'", 89 | uniqueLowerTicker, 90 | body.BRC20Max, 91 | ) 92 | return errors.New("deploy, but max invalid") 93 | } else { 94 | if max.Sign() < 0 || max.IsOverflowUint64() { 95 | return nil 96 | // return errors.New("deploy, but max invalid (range)") 97 | } 98 | 99 | if max.Sign() == 0 { 100 | if tinfo.SelfMint { 101 | tinfo.Max = max.GetMaxUint64() 102 | } else { 103 | return errors.New("deploy, but max invalid (0)") 104 | } 105 | } else { 106 | tinfo.Max = max 107 | } 108 | } 109 | 110 | // lim 111 | if lim, err := decimal.NewDecimalFromString(tinfo.Data.BRC20Limit, int(tinfo.Decimal)); err != nil { 112 | // limit invalid 113 | log.Printf("deploy, but limit invalid. ticker: %s, limit: '%s'", 114 | uniqueLowerTicker, 115 | tinfo.Data.BRC20Limit, 116 | ) 117 | return errors.New("deploy, but lim invalid") 118 | } else { 119 | if lim.Sign() < 0 || lim.IsOverflowUint64() { 120 | return errors.New("deploy, but lim invalid (range)") 121 | } 122 | if lim.Sign() == 0 { 123 | if tinfo.SelfMint { 124 | tinfo.Limit = lim.GetMaxUint64() 125 | } else { 126 | return errors.New("deploy, but lim invalid (0)") 127 | } 128 | } else { 129 | tinfo.Limit = lim 130 | } 131 | } 132 | 133 | tokenInfo := &model.BRC20TokenInfo{Ticker: body.BRC20Tick, Deploy: tinfo} 134 | g.InscriptionsTickerInfoMap[uniqueLowerTicker] = tokenInfo 135 | 136 | tokenBalance := &model.BRC20TokenBalance{Ticker: body.BRC20Tick, PkScript: data.PkScript} 137 | 138 | if g.EnableHistory { 139 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_INSCRIBE_DEPLOY, true, false, tinfo, nil, data) 140 | history := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 141 | 142 | tokenBalance.History = append(tokenBalance.History, history) 143 | tokenInfo.History = append(tokenInfo.History, history) 144 | 145 | // user history 146 | userHistory := g.GetBRC20HistoryByUser(string(data.PkScript)) 147 | userHistory.History = append(userHistory.History, history) 148 | // all history 149 | g.AllHistory = append(g.AllHistory, history) 150 | } 151 | 152 | // mark update 153 | tokenInfo.UpdateHeight = data.Height 154 | 155 | // init user tokens 156 | var userTokens map[string]*model.BRC20TokenBalance 157 | if tokens, ok := g.UserTokensBalanceData[string(data.PkScript)]; !ok { 158 | userTokens = make(map[string]*model.BRC20TokenBalance, 0) 159 | g.UserTokensBalanceData[string(data.PkScript)] = userTokens 160 | } else { 161 | userTokens = tokens 162 | } 163 | userTokens[uniqueLowerTicker] = tokenBalance 164 | 165 | // init token users 166 | tokenUsers := make(map[string]*model.BRC20TokenBalance, 0) 167 | tokenUsers[string(data.PkScript)] = tokenBalance 168 | g.TokenUsersBalanceData[uniqueLowerTicker] = tokenUsers 169 | 170 | g.InscriptionsValidBRC20DataMap[data.CreateIdxKey] = tinfo.Data 171 | return nil 172 | } 173 | -------------------------------------------------------------------------------- /indexer/brc20_mint.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/constant" 9 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func (g *BRC20ModuleIndexer) ProcessMint(data *model.InscriptionBRC20Data) error { 15 | body := new(model.InscriptionBRC20MintTransferContent) 16 | if err := body.Unmarshal(data.ContentBody); err != nil { 17 | return nil 18 | } 19 | 20 | // check tick 21 | uniqueLowerTicker, err := utils.GetValidUniqueLowerTickerTicker(body.BRC20Tick) 22 | if err != nil { 23 | return nil 24 | // return errors.New("mint, tick length not 4 or 5") 25 | } 26 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 27 | if !ok { 28 | return nil 29 | // return errors.New(fmt.Sprintf("mint %s, but tick not exist", body.BRC20Tick)) 30 | } 31 | tinfo := tokenInfo.Deploy 32 | if tinfo.SelfMint { 33 | if utils.DecodeInscriptionFromBin(data.Parent) != tinfo.GetInscriptionId() { 34 | return errors.New(fmt.Sprintf("self mint %s, but parent invalid", body.BRC20Tick)) 35 | } 36 | } 37 | 38 | // check mint amount 39 | amt, err := decimal.NewDecimalFromString(body.BRC20Amount, int(tinfo.Decimal)) 40 | if err != nil { 41 | return errors.New(fmt.Sprintf("mint %s, but invalid amount(%s)", body.BRC20Tick, body.BRC20Amount)) 42 | } 43 | if amt.Sign() <= 0 || amt.Cmp(tinfo.Limit) > 0 { 44 | return errors.New(fmt.Sprintf("mint %s, invalid amount(%s), limit(%s)", body.BRC20Tick, body.BRC20Amount, tinfo.Limit)) 45 | } 46 | 47 | // get user's tokens to update 48 | tokenBalance := g.GetUserTokenBalance(tokenInfo.Ticker, string(data.PkScript)) 49 | 50 | body.BRC20Tick = tokenInfo.Ticker 51 | mintInfo := model.NewInscriptionBRC20TickInfo(body.BRC20Tick, body.Operation, data) 52 | mintInfo.Data.BRC20Amount = body.BRC20Amount 53 | mintInfo.Data.BRC20Minted = amt.String() 54 | mintInfo.Decimal = tinfo.Decimal 55 | mintInfo.Amount = amt 56 | if tinfo.TotalMinted.Cmp(tinfo.Max) >= 0 { 57 | // invalid history 58 | if g.EnableHistory { 59 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_INSCRIBE_MINT, false, false, mintInfo, tokenBalance, data) 60 | history := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 61 | 62 | tokenBalance.History = append(tokenBalance.History, history) 63 | tokenBalance.HistoryMint = append(tokenBalance.HistoryMint, history) 64 | tokenInfo.History = append(tokenInfo.History, history) 65 | tokenInfo.HistoryMint = append(tokenInfo.HistoryMint, history) 66 | } 67 | return errors.New(fmt.Sprintf("mint %s, but mint out", body.BRC20Tick)) 68 | } 69 | 70 | // update tinfo 71 | tokenInfo.UpdateHeight = data.Height 72 | 73 | // minted 74 | balanceMinted := decimal.NewDecimalCopy(amt) 75 | if tinfo.TotalMinted.Add(amt).Cmp(tinfo.Max) > 0 { 76 | balanceMinted = tinfo.Max.Sub(tinfo.TotalMinted) 77 | } 78 | tinfo.TotalMinted = tinfo.TotalMinted.Add(balanceMinted) 79 | if tinfo.TotalMinted.Cmp(tinfo.Max) >= 0 { 80 | tinfo.CompleteHeight = data.Height 81 | tinfo.CompleteBlockTime = data.BlockTime 82 | } 83 | // confirmed minted 84 | now := time.Now() 85 | if data.BlockTime > 0 { 86 | tinfo.ConfirmedMinted = tinfo.ConfirmedMinted.Add(balanceMinted) 87 | if data.BlockTime < uint32(now.Unix())-3600 { 88 | tinfo.ConfirmedMinted1h = tinfo.ConfirmedMinted1h.Add(balanceMinted) 89 | } 90 | if data.BlockTime < uint32(now.Unix())-86400 { 91 | tinfo.ConfirmedMinted24h = tinfo.ConfirmedMinted24h.Add(balanceMinted) 92 | } 93 | } 94 | // count 95 | tinfo.MintTimes++ 96 | tinfo.Data.BRC20Minted = tinfo.TotalMinted.String() 97 | // valid mint inscriptionNumber range 98 | tinfo.InscriptionNumberEnd = data.InscriptionNumber 99 | 100 | // update mint info 101 | mintInfo.Data.BRC20Minted = balanceMinted.String() 102 | mintInfo.Amount = balanceMinted 103 | 104 | // update tokenBalance 105 | tokenBalance.UpdateHeight = data.Height 106 | 107 | if data.BlockTime > 0 { 108 | tokenBalance.AvailableBalanceSafe = tokenBalance.AvailableBalanceSafe.Add(balanceMinted) 109 | } 110 | tokenBalance.AvailableBalance = tokenBalance.AvailableBalance.Add(balanceMinted) 111 | 112 | // burn 113 | if len(data.PkScript) == 1 && data.PkScript[0] == 0x6a { 114 | tinfo.Burned = tinfo.Burned.Add(balanceMinted) 115 | } 116 | 117 | if g.EnableHistory { 118 | // history 119 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_INSCRIBE_MINT, true, false, mintInfo, tokenBalance, data) 120 | history := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 121 | 122 | // tick history 123 | tokenBalance.History = append(tokenBalance.History, history) 124 | tokenBalance.HistoryMint = append(tokenBalance.HistoryMint, history) 125 | tokenInfo.History = append(tokenInfo.History, history) 126 | tokenInfo.HistoryMint = append(tokenInfo.HistoryMint, history) 127 | // user address 128 | userHistory := g.GetBRC20HistoryByUser(string(data.PkScript)) 129 | userHistory.History = append(userHistory.History, history) 130 | // all history 131 | g.AllHistory = append(g.AllHistory, history) 132 | } 133 | // g.InscriptionsValidBRC20DataMap[data.CreateIdxKey] = mintInfo.Data 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /indexer/brc20_transfer.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strings" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/conf" 9 | "github.com/unisat-wallet/libbrc20-indexer/constant" 10 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 11 | "github.com/unisat-wallet/libbrc20-indexer/model" 12 | "github.com/unisat-wallet/libbrc20-indexer/utils" 13 | ) 14 | 15 | func (g *BRC20ModuleIndexer) GetTransferInfoByKey(createIdxKey string) ( 16 | transferInfo *model.InscriptionBRC20TickInfo, isInvalid bool) { 17 | var ok bool 18 | // transfer 19 | transferInfo, ok = g.InscriptionsValidTransferMap[createIdxKey] 20 | if !ok { 21 | transferInfo, ok = g.InscriptionsInvalidTransferMap[createIdxKey] 22 | if !ok { 23 | transferInfo = nil 24 | } else { 25 | // don't remove. use for api valid data 26 | // delete(g.InscriptionsInvalidTransferMap, createIdxKey) 27 | } 28 | isInvalid = true 29 | } else { 30 | // don't remove. use for api valid data 31 | // delete(g.InscriptionsValidTransferMap, createIdxKey) 32 | } 33 | 34 | return transferInfo, isInvalid 35 | } 36 | 37 | func (g *BRC20ModuleIndexer) ProcessTransfer(data *model.InscriptionBRC20Data, transferInfo *model.InscriptionBRC20TickInfo, isInvalid bool) error { 38 | // ticker 39 | uniqueLowerTicker := strings.ToLower(transferInfo.Tick) 40 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 41 | if !ok { 42 | log.Printf("ProcessBRC20Transfer send transfer, but ticker invalid. txid: %s", 43 | utils.HashString([]byte(data.TxId)), 44 | ) 45 | return errors.New("transfer, invalid ticker") 46 | } 47 | 48 | // to 49 | senderPkScript := string(transferInfo.PkScript) 50 | receiverPkScript := string(data.PkScript) 51 | if data.Satoshi == 0 { 52 | receiverPkScript = senderPkScript 53 | data.PkScript = senderPkScript 54 | } 55 | 56 | // global history 57 | if g.EnableHistory { 58 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_TRANSFER, !isInvalid, true, transferInfo, nil, data) 59 | history := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 60 | 61 | tokenInfo.History = append(tokenInfo.History, history) 62 | tokenInfo.HistoryTransfer = append(tokenInfo.HistoryTransfer, history) 63 | if !isInvalid { 64 | // all history 65 | g.AllHistory = append(g.AllHistory, history) 66 | } 67 | } 68 | 69 | // from 70 | // get user's tokens to update 71 | fromUserTokens, ok := g.UserTokensBalanceData[senderPkScript] 72 | if !ok { 73 | log.Printf("ProcessBRC20Transfer send from user missing. height: %d, txidx: %d", 74 | data.Height, 75 | data.TxIdx, 76 | ) 77 | return errors.New("transfer, invalid from data") 78 | } 79 | // get tokenBalance to update 80 | fromTokenBalance, ok := fromUserTokens[uniqueLowerTicker] 81 | if !ok { 82 | log.Printf("ProcessBRC20Transfer send from ticker missing. height: %d, txidx: %d", 83 | data.Height, 84 | data.TxIdx, 85 | ) 86 | return errors.New("transfer, invalid from balance") 87 | } 88 | 89 | if isInvalid { 90 | if g.EnableHistory { 91 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_SEND, false, true, transferInfo, fromTokenBalance, data) 92 | fromHistory := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 93 | 94 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 95 | fromTokenBalance.HistorySend = append(fromTokenBalance.HistorySend, fromHistory) 96 | 97 | userHistory := g.GetBRC20HistoryByUser(senderPkScript) 98 | userHistory.History = append(userHistory.History, fromHistory) 99 | } 100 | return nil 101 | } 102 | 103 | if _, ok := fromTokenBalance.ValidTransferMap[data.CreateIdxKey]; !ok { 104 | log.Printf("ProcessBRC20Transfer send from transfer missing(dup transfer?). height: %d, txidx: %d", 105 | data.Height, 106 | data.TxIdx, 107 | ) 108 | return errors.New("transfer, invalid transfer") 109 | } 110 | 111 | // set from 112 | fromTokenBalance.UpdateHeight = data.Height 113 | 114 | fromTokenBalance.TransferableBalance = fromTokenBalance.TransferableBalance.Sub(transferInfo.Amount) 115 | delete(fromTokenBalance.ValidTransferMap, data.CreateIdxKey) 116 | 117 | if g.EnableHistory { 118 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_SEND, true, true, transferInfo, fromTokenBalance, data) 119 | fromHistory := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 120 | 121 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 122 | fromTokenBalance.HistorySend = append(fromTokenBalance.HistorySend, fromHistory) 123 | 124 | userHistoryFrom := g.GetBRC20HistoryByUser(senderPkScript) 125 | userHistoryFrom.History = append(userHistoryFrom.History, fromHistory) 126 | } 127 | 128 | // to 129 | // get user's tokens to update 130 | tokenBalance := g.GetUserTokenBalance(transferInfo.Tick, receiverPkScript) 131 | // set to 132 | tokenBalance.UpdateHeight = data.Height 133 | 134 | if data.BlockTime > 0 { 135 | tokenBalance.AvailableBalanceSafe = tokenBalance.AvailableBalanceSafe.Add(transferInfo.Amount) 136 | } 137 | tokenBalance.AvailableBalance = tokenBalance.AvailableBalance.Add(transferInfo.Amount) 138 | 139 | // burn 140 | if len(receiverPkScript) == 1 && []byte(receiverPkScript)[0] == 0x6a { 141 | tokenInfo.Deploy.Burned = tokenInfo.Deploy.Burned.Add(transferInfo.Amount) 142 | } 143 | 144 | if g.EnableHistory { 145 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_RECEIVE, true, true, transferInfo, tokenBalance, data) 146 | toHistory := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 147 | 148 | tokenBalance.History = append(tokenBalance.History, toHistory) 149 | tokenBalance.HistoryReceive = append(tokenBalance.HistoryReceive, toHistory) 150 | 151 | userHistoryTo := g.GetBRC20HistoryByUser(receiverPkScript) 152 | userHistoryTo.History = append(userHistoryTo.History, toHistory) 153 | } 154 | 155 | //////////////////////////////////////////////////////////////// 156 | // skip module deposit if self mint for now 157 | if tokenInfo.Deploy.SelfMint { 158 | return nil 159 | } 160 | 161 | //////////////////////////////////////////////////////////////// 162 | // module conditional approve (black withdraw) 163 | if g.ThisTxId != data.TxId { 164 | g.TxStaticTransferStatesForConditionalApprove = nil 165 | g.ThisTxId = data.TxId 166 | } 167 | 168 | if data.Height < conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 169 | inscriptionId := transferInfo.Meta.GetInscriptionId() 170 | events := g.GenerateApproveEventsByTransfer(inscriptionId, transferInfo.Tick, senderPkScript, receiverPkScript, transferInfo.Amount) 171 | if err := g.ProcessConditionalApproveEvents(events); err != nil { 172 | return err 173 | } 174 | } 175 | //////////////////////////////////////////////////////////////// 176 | // module deposit 177 | moduleId, ok := utils.GetModuleFromScript([]byte(receiverPkScript)) 178 | if !ok { 179 | // errors.New("module transfer, not module") 180 | return nil 181 | } 182 | moduleInfo, ok := g.ModulesInfoMap[moduleId] 183 | if !ok { // invalid module 184 | return nil 185 | // return errors.New(fmt.Sprintf("module transfer, module(%s) not exist", moduleId)) 186 | } 187 | 188 | // global history 189 | mHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_TYPE_N_TRANSFER, transferInfo.Meta, data, nil, true) 190 | moduleInfo.History = append(moduleInfo.History, mHistory) 191 | 192 | // get user's tokens to update 193 | 194 | moduleTokenBalance := moduleInfo.GetUserTokenBalance(transferInfo.Tick, senderPkScript) 195 | moduleTokenBalance.UpdateHeight = data.Height 196 | // set module deposit 197 | if data.BlockTime > 0 { // how many confirmes ok 198 | moduleTokenBalance.SwapAccountBalanceSafe = moduleTokenBalance.SwapAccountBalanceSafe.Add(transferInfo.Amount) 199 | } 200 | moduleTokenBalance.SwapAccountBalance = moduleTokenBalance.SwapAccountBalance.Add(transferInfo.Amount) 201 | 202 | // record state 203 | stateBalance := moduleInfo.GetTickConditionalApproveStateBalance(transferInfo.Tick) 204 | stateBalance.BalanceDeposite = stateBalance.BalanceDeposite.Add(transferInfo.Amount) 205 | 206 | return nil 207 | } 208 | 209 | func (g *BRC20ModuleIndexer) ProcessInscribeTransfer(data *model.InscriptionBRC20Data) error { 210 | body := new(model.InscriptionBRC20MintTransferContent) 211 | if err := body.Unmarshal(data.ContentBody); err != nil { 212 | return nil 213 | } 214 | 215 | // check tick 216 | uniqueLowerTicker, err := utils.GetValidUniqueLowerTickerTicker(body.BRC20Tick) 217 | if err != nil { 218 | return nil 219 | // return errors.New("transfer, tick length not 4 or 5") 220 | } 221 | 222 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 223 | if !ok { 224 | return nil 225 | // return errors.New(fmt.Sprintf("transfer %s, but tick not exist", body.BRC20Tick)) 226 | } 227 | tinfo := tokenInfo.Deploy 228 | 229 | // check amount 230 | amt, err := decimal.NewDecimalFromString(body.BRC20Amount, int(tinfo.Decimal)) 231 | if err != nil { 232 | return nil 233 | // return errors.New("transfer, but invalid amount") 234 | } 235 | if amt.Sign() <= 0 || amt.Cmp(tinfo.Max) > 0 { 236 | return nil 237 | // return errors.New("transfer, invalid amount(range)") 238 | } 239 | 240 | balanceTransfer := decimal.NewDecimalCopy(amt) 241 | 242 | // get user's tokens to update 243 | var userTokens map[string]*model.BRC20TokenBalance 244 | if tokens, ok := g.UserTokensBalanceData[string(data.PkScript)]; !ok { 245 | userTokens = make(map[string]*model.BRC20TokenBalance, 0) 246 | g.UserTokensBalanceData[string(data.PkScript)] = userTokens 247 | } else { 248 | userTokens = tokens 249 | } 250 | // get tokenBalance to update 251 | var tokenBalance *model.BRC20TokenBalance 252 | if token, ok := userTokens[uniqueLowerTicker]; !ok { 253 | tokenBalance = &model.BRC20TokenBalance{Ticker: tokenInfo.Ticker, PkScript: data.PkScript} 254 | userTokens[uniqueLowerTicker] = tokenBalance 255 | } else { 256 | tokenBalance = token 257 | } 258 | // set token's users 259 | tokenUsers, ok := g.TokenUsersBalanceData[uniqueLowerTicker] 260 | if !ok { 261 | log.Panicf("g.TokenUsersBalanceData[%s] not exist, tick: %s", uniqueLowerTicker, uniqueLowerTicker) 262 | } 263 | tokenUsers[string(data.PkScript)] = tokenBalance 264 | 265 | body.BRC20Tick = tokenInfo.Ticker 266 | 267 | transferInfo := model.NewInscriptionBRC20TickInfo(body.BRC20Tick, body.Operation, data) 268 | transferInfo.Data.BRC20Amount = body.BRC20Amount 269 | transferInfo.Data.BRC20Limit = tinfo.Data.BRC20Limit 270 | transferInfo.Data.BRC20Decimal = tinfo.Data.BRC20Decimal 271 | 272 | transferInfo.Tick = tokenInfo.Ticker 273 | transferInfo.Amount = balanceTransfer 274 | transferInfo.Meta = data 275 | 276 | if g.EnableHistory { 277 | history := g.HistoryCount 278 | historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_INSCRIBE_TRANSFER, true, false, transferInfo, tokenBalance, data) 279 | // If use the safe version of the available balance, it will cause the unconfirmed balance to not be able to be used to create a valid transfer inscription. 280 | if tokenBalance.AvailableBalance.Cmp(balanceTransfer) < 0 { 281 | historyObj.Valid = false 282 | // user history 283 | tokenBalance.History = append(tokenBalance.History, history) 284 | tokenBalance.HistoryInscribeTransfer = append(tokenBalance.HistoryInscribeTransfer, history) 285 | // global history 286 | tokenInfo.History = append(tokenInfo.History, history) 287 | tokenInfo.HistoryInscribeTransfer = append(tokenInfo.HistoryInscribeTransfer, history) 288 | 289 | userHistory := g.GetBRC20HistoryByUser(string(data.PkScript)) 290 | userHistory.History = append(userHistory.History, history) 291 | } else { 292 | historyObj.Valid = true 293 | // user tick history 294 | tokenBalance.History = append(tokenBalance.History, history) 295 | tokenBalance.HistoryInscribeTransfer = append(tokenBalance.HistoryInscribeTransfer, history) 296 | // user history 297 | userHistory := g.GetBRC20HistoryByUser(string(data.PkScript)) 298 | userHistory.History = append(userHistory.History, history) 299 | // global history 300 | tokenInfo.History = append(tokenInfo.History, history) 301 | tokenInfo.HistoryInscribeTransfer = append(tokenInfo.HistoryInscribeTransfer, history) 302 | // all history 303 | g.AllHistory = append(g.AllHistory, history) 304 | } 305 | 306 | g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 307 | } 308 | 309 | // If use the safe version of the available balance, it will cause the unconfirmed balance to not be able to be used to create a valid transfer inscription. 310 | if tokenBalance.AvailableBalance.Cmp(balanceTransfer) < 0 { 311 | g.InscriptionsInvalidTransferMap[data.CreateIdxKey] = transferInfo 312 | } else { 313 | // Update available balance 314 | 315 | // fixme: The available safe balance may not decrease, the current transfer usage of available balance source is not accurately distinguished. 316 | tokenBalance.AvailableBalanceSafe = tokenBalance.AvailableBalanceSafe.Sub(balanceTransfer) 317 | 318 | tokenBalance.AvailableBalance = tokenBalance.AvailableBalance.Sub(balanceTransfer) 319 | tokenBalance.TransferableBalance = tokenBalance.TransferableBalance.Add(balanceTransfer) 320 | 321 | if tokenBalance.ValidTransferMap == nil { 322 | tokenBalance.ValidTransferMap = make(map[string]*model.InscriptionBRC20TickInfo, 1) 323 | } 324 | tokenBalance.ValidTransferMap[data.CreateIdxKey] = transferInfo 325 | g.InscriptionsValidTransferMap[data.CreateIdxKey] = transferInfo 326 | g.InscriptionsValidBRC20DataMap[data.CreateIdxKey] = transferInfo.Data 327 | } 328 | 329 | return nil 330 | } 331 | -------------------------------------------------------------------------------- /indexer/indexer.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | "github.com/unisat-wallet/libbrc20-indexer/conf" 8 | "github.com/unisat-wallet/libbrc20-indexer/constant" 9 | "github.com/unisat-wallet/libbrc20-indexer/model" 10 | ) 11 | 12 | func isJson(contentBody []byte) bool { 13 | if len(contentBody) < 40 { 14 | return false 15 | } 16 | 17 | content := bytes.TrimSpace(contentBody) 18 | if !bytes.HasPrefix(content, []byte("{")) { 19 | return false 20 | } 21 | if !bytes.HasSuffix(content, []byte("}")) { 22 | return false 23 | } 24 | 25 | return true 26 | } 27 | 28 | // ProcessUpdateLatestBRC20Loop 29 | func (g *BRC20ModuleIndexer) ProcessUpdateLatestBRC20Loop(brc20Datas, brc20DatasDump chan interface{}) { 30 | if brc20Datas == nil { 31 | return 32 | } 33 | 34 | g.Durty = false 35 | for dataIn := range brc20Datas { 36 | 37 | for { 38 | data := dataIn.(*model.InscriptionBRC20Data) 39 | 40 | // update latest height 41 | g.BestHeight = data.Height 42 | 43 | // is sending transfer 44 | if data.IsTransfer { 45 | 46 | if data.Height < conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 47 | // module conditional approve 48 | if condApproveInfo, isInvalid := g.GetConditionalApproveInfoByKey(data.CreateIdxKey); condApproveInfo != nil { 49 | if err := g.ProcessConditionalApprove(data, condApproveInfo, isInvalid); err != nil { 50 | log.Printf("process conditional approve move failed: %s", err) 51 | } else { 52 | g.Durty = true 53 | } 54 | break 55 | } 56 | } 57 | 58 | // not first move 59 | if data.Sequence != 1 { 60 | break 61 | } 62 | 63 | // transfer 64 | if transferInfo, isInvalid := g.GetTransferInfoByKey(data.CreateIdxKey); transferInfo != nil { 65 | g.InscriptionsTransferRemoveMap[data.CreateIdxKey] = data.Height 66 | g.Durty = true 67 | 68 | if err := g.ProcessTransfer(data, transferInfo, isInvalid); err != nil { 69 | log.Printf("process transfer move failed: %s", err) 70 | } 71 | break 72 | } 73 | 74 | // module approve 75 | if approveInfo, isInvalid := g.GetApproveInfoByKey(data.CreateIdxKey); approveInfo != nil { 76 | g.InscriptionsApproveRemoveMap[data.CreateIdxKey] = data.Height 77 | g.Durty = true 78 | 79 | if err := g.ProcessApprove(data, approveInfo, isInvalid); err != nil { 80 | log.Printf("process approve move failed: %s", err) 81 | } 82 | break 83 | } 84 | 85 | // module withdraw 86 | if withdrawInfo := g.GetWithdrawInfoByKey(data.CreateIdxKey); withdrawInfo != nil { 87 | g.InscriptionsWithdrawRemoveMap[data.CreateIdxKey] = data.Height 88 | g.Durty = true 89 | 90 | if err := g.ProcessWithdraw(data, withdrawInfo); err != nil { 91 | log.Printf("process withdraw move failed: %s", err) 92 | } else { 93 | g.InscriptionsValidWithdrawMap[withdrawInfo.Data.GetInscriptionId()] = data.Height 94 | } 95 | break 96 | } 97 | 98 | // module commit 99 | if commitFrom, isInvalid := g.GetCommitInfoByKey(data.CreateIdxKey); commitFrom != nil { 100 | g.InscriptionsCommitRemoveMap[data.CreateIdxKey] = data.Height 101 | g.Durty = true 102 | 103 | if err := g.ProcessCommit(commitFrom, data, isInvalid); err != nil { 104 | log.Printf("process commit move failed: %s", err) 105 | } 106 | break 107 | } 108 | 109 | break 110 | } 111 | 112 | // inscribe as fee 113 | if data.Satoshi == 0 { 114 | break 115 | } 116 | 117 | if ok := isJson(data.ContentBody); !ok { 118 | // log.Println("not json") 119 | break 120 | } 121 | 122 | // protocal, lower case only 123 | body := new(model.InscriptionBRC20ProtocalContent) 124 | if err := body.Unmarshal(data.ContentBody); err != nil { 125 | // log.Println("Unmarshal failed", err, string(data.ContentBody)) 126 | break 127 | } 128 | 129 | // is inscribe deploy/mint/transfer 130 | if body.Proto != constant.BRC20_P && 131 | body.Proto != constant.BRC20_P_MODULE && 132 | body.Proto != constant.BRC20_P_SWAP { 133 | // log.Println("not proto") 134 | break 135 | } 136 | 137 | var process func(*model.InscriptionBRC20Data) error 138 | if body.Proto == constant.BRC20_P && body.Operation == constant.BRC20_OP_DEPLOY { 139 | process = g.ProcessDeploy 140 | } else if body.Proto == constant.BRC20_P && body.Operation == constant.BRC20_OP_MINT { 141 | process = g.ProcessMint 142 | } else if body.Proto == constant.BRC20_P && body.Operation == constant.BRC20_OP_TRANSFER { 143 | process = g.ProcessInscribeTransfer 144 | } else if body.Proto == constant.BRC20_P_MODULE && body.Operation == constant.BRC20_OP_MODULE_DEPLOY { 145 | process = g.ProcessCreateModule 146 | } else if body.Proto == constant.BRC20_P_MODULE && body.Operation == constant.BRC20_OP_MODULE_WITHDRAW { 147 | process = g.ProcessInscribeWithdraw 148 | } else if body.Proto == constant.BRC20_P_SWAP && body.Operation == constant.BRC20_OP_SWAP_APPROVE { 149 | process = g.ProcessInscribeApprove 150 | } else if body.Proto == constant.BRC20_P_SWAP && body.Operation == constant.BRC20_OP_SWAP_CONDITIONAL_APPROVE { 151 | process = g.ProcessInscribeConditionalApprove 152 | } else if body.Proto == constant.BRC20_P_SWAP && body.Operation == constant.BRC20_OP_SWAP_COMMIT { 153 | process = g.ProcessInscribeCommit 154 | } else { 155 | break 156 | } 157 | 158 | if err := process(data); err != nil { 159 | if body.Operation == constant.BRC20_OP_MINT { 160 | if conf.DEBUG { 161 | log.Printf("(%d) process failed: %s", g.BestHeight, err) 162 | } 163 | } else { 164 | log.Printf("(%d) process failed: %s", g.BestHeight, err) 165 | } 166 | } else { 167 | g.Durty = true 168 | } 169 | break 170 | } 171 | if brc20DatasDump != nil { 172 | brc20DatasDump <- dataIn 173 | } 174 | } 175 | 176 | for _, holdersBalanceMap := range g.TokenUsersBalanceData { 177 | for key, balance := range holdersBalanceMap { 178 | if balance.AvailableBalance.Sign() == 0 && balance.TransferableBalance.Sign() == 0 { 179 | delete(holdersBalanceMap, key) 180 | } 181 | } 182 | } 183 | if !g.Durty { 184 | return 185 | } 186 | 187 | log.Printf("process swap finish. ticker: %d, users: %d, tokens: %d, validInscription: %d, validTransfer: %d, invalidTransfer: %d, history: %d", 188 | len(g.InscriptionsTickerInfoMap), 189 | len(g.UserTokensBalanceData), 190 | len(g.TokenUsersBalanceData), 191 | 192 | len(g.InscriptionsValidBRC20DataMap), 193 | 194 | len(g.InscriptionsValidTransferMap), 195 | len(g.InscriptionsInvalidTransferMap), 196 | 197 | g.HistoryCount, 198 | ) 199 | 200 | nswap := 0 201 | for _, m := range g.ModulesInfoMap { 202 | nswap += len(m.SwapPoolTotalBalanceDataMap) 203 | } 204 | 205 | nuser := 0 206 | for _, m := range g.ModulesInfoMap { 207 | nuser += len(m.UsersTokenBalanceDataMap) 208 | } 209 | 210 | log.Printf("process swap finish. module: %d, swap: %d, users: %d, validApprove: %d, invalidApprove: %d, validCommit: %d, invalidCommit: %d", 211 | len(g.ModulesInfoMap), 212 | nswap, 213 | nuser, 214 | 215 | len(g.InscriptionsValidApproveMap), 216 | len(g.InscriptionsInvalidApproveMap), 217 | 218 | len(g.InscriptionsValidCommitMap), 219 | len(g.InscriptionsInvalidCommitMap), 220 | ) 221 | } 222 | 223 | func (g *BRC20ModuleIndexer) Init() { 224 | g.initBRC20() 225 | g.initModule() 226 | } 227 | -------------------------------------------------------------------------------- /indexer/module_approve.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/unisat-wallet/libbrc20-indexer/constant" 12 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 13 | "github.com/unisat-wallet/libbrc20-indexer/model" 14 | "github.com/unisat-wallet/libbrc20-indexer/utils" 15 | ) 16 | 17 | func (g *BRC20ModuleIndexer) GetApproveInfoByKey(createIdxKey string) ( 18 | approveInfo *model.InscriptionBRC20SwapInfo, isInvalid bool) { 19 | var ok bool 20 | // approve 21 | approveInfo, ok = g.InscriptionsValidApproveMap[createIdxKey] 22 | if !ok { 23 | approveInfo, ok = g.InscriptionsInvalidApproveMap[createIdxKey] 24 | if !ok { 25 | approveInfo = nil 26 | } 27 | isInvalid = true 28 | } 29 | 30 | return approveInfo, isInvalid 31 | } 32 | 33 | func (g *BRC20ModuleIndexer) ProcessApprove(data *model.InscriptionBRC20Data, approveInfo *model.InscriptionBRC20SwapInfo, isInvalid bool) error { 34 | // ticker 35 | uniqueLowerTicker := strings.ToLower(approveInfo.Tick) 36 | if _, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker]; !ok { 37 | return errors.New("approve, invalid ticker") 38 | } 39 | 40 | moduleInfo, ok := g.ModulesInfoMap[approveInfo.Module] 41 | if !ok { 42 | log.Printf("ProcessBRC20Approve send approve, but ticker invalid. txid: %s", 43 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 44 | ) 45 | return errors.New("approve, module invalid") 46 | } 47 | 48 | // from 49 | // get user's tokens to update 50 | fromUserTokens, ok := moduleInfo.UsersTokenBalanceDataMap[string(approveInfo.Data.PkScript)] 51 | if !ok { 52 | log.Printf("ProcessBRC20Approve send from user missing. height: %d, txidx: %d", 53 | data.Height, 54 | data.TxIdx, 55 | ) 56 | return errors.New("approve, send from user missing") 57 | } 58 | // get tokenBalance to update 59 | fromTokenBalance, ok := fromUserTokens[uniqueLowerTicker] 60 | if !ok { 61 | log.Printf("ProcessBRC20Approve send from ticker missing. height: %d, txidx: %d", 62 | data.Height, 63 | data.TxIdx, 64 | ) 65 | return errors.New("approve, send from ticker missing") 66 | } 67 | 68 | // Cross-check whether the approve-inscription exists. 69 | if _, ok := fromTokenBalance.ValidApproveMap[data.CreateIdxKey]; !ok { 70 | log.Printf("ProcessBRC20Approve send from approve missing(dup approve?). height: %d, txidx: %d", 71 | data.Height, 72 | data.TxIdx, 73 | ) 74 | return errors.New("approve, send from approve missing(dup)") 75 | } 76 | 77 | // to address 78 | receiverPkScript := string(data.PkScript) 79 | if data.Satoshi == 0 { 80 | receiverPkScript = string(approveInfo.Data.PkScript) 81 | data.PkScript = receiverPkScript 82 | } 83 | 84 | // global history 85 | historyData := &model.BRC20SwapHistoryApproveData{ 86 | Tick: approveInfo.Tick, 87 | Amount: approveInfo.Amount.String(), 88 | } 89 | history := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_APPROVE, approveInfo.Data, data, historyData, !isInvalid) 90 | moduleInfo.History = append(moduleInfo.History, history) 91 | if isInvalid { 92 | // from invalid history 93 | fromHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_APPROVE_FROM, approveInfo.Data, data, nil, false) 94 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 95 | return nil 96 | } 97 | 98 | // to 99 | tokenBalance := moduleInfo.GetUserTokenBalance(approveInfo.Tick, receiverPkScript) 100 | 101 | // set from 102 | fromTokenBalance.UpdateHeight = g.BestHeight 103 | 104 | fromTokenBalance.ApproveableBalance = fromTokenBalance.ApproveableBalance.Sub(approveInfo.Amount) 105 | delete(fromTokenBalance.ValidApproveMap, data.CreateIdxKey) 106 | 107 | fromHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_APPROVE_FROM, approveInfo.Data, data, nil, true) 108 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 109 | 110 | // set to 111 | tokenBalance.UpdateHeight = g.BestHeight 112 | if data.BlockTime > 0 { 113 | tokenBalance.SwapAccountBalanceSafe = tokenBalance.SwapAccountBalanceSafe.Add(approveInfo.Amount) 114 | } 115 | tokenBalance.SwapAccountBalance = tokenBalance.SwapAccountBalance.Add(approveInfo.Amount) 116 | 117 | toHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_APPROVE_TO, approveInfo.Data, data, nil, true) 118 | tokenBalance.History = append(tokenBalance.History, toHistory) 119 | 120 | return nil 121 | } 122 | 123 | func (g *BRC20ModuleIndexer) ProcessInscribeApprove(data *model.InscriptionBRC20Data) error { 124 | var body model.InscriptionBRC20ModuleSwapApproveContent 125 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 126 | log.Printf("parse approve json failed. txid: %s", 127 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 128 | ) 129 | return err 130 | } 131 | 132 | // lower case moduleid only 133 | if body.Module != strings.ToLower(body.Module) { 134 | return errors.New("module id invalid") 135 | } 136 | 137 | moduleInfo, ok := g.ModulesInfoMap[body.Module] 138 | if !ok { // invalid module 139 | return errors.New("module invalid") 140 | } 141 | 142 | if len(body.Tick) != 4 { 143 | return errors.New("tick invalid") 144 | } 145 | 146 | uniqueLowerTicker := strings.ToLower(body.Tick) 147 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 148 | if !ok { 149 | return errors.New("tick not exist") 150 | } 151 | tinfo := tokenInfo.Deploy 152 | 153 | amt, err := decimal.NewDecimalFromString(body.Amount, int(tinfo.Decimal)) 154 | if err != nil { 155 | return errors.New(fmt.Sprintf("approve amount invalid: %s", body.Amount)) 156 | } 157 | if amt.Sign() <= 0 || amt.Cmp(tinfo.Max) > 0 { 158 | return errors.New("amount out of range") 159 | } 160 | 161 | balanceApprove := decimal.NewDecimalCopy(amt) 162 | 163 | // Unify ticker case 164 | body.Tick = tokenInfo.Ticker 165 | // Set up approve data for subsequent use. 166 | approveInfo := &model.InscriptionBRC20SwapInfo{ 167 | Data: data, 168 | } 169 | approveInfo.Module = body.Module 170 | approveInfo.Tick = tokenInfo.Ticker 171 | approveInfo.Amount = balanceApprove 172 | 173 | // global history 174 | historyData := &model.BRC20SwapHistoryApproveData{ 175 | Tick: approveInfo.Tick, 176 | Amount: approveInfo.Amount.String(), 177 | } 178 | history := model.NewBRC20ModuleHistory(false, constant.BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_APPROVE, data, data, historyData, true) 179 | moduleInfo.History = append(moduleInfo.History, history) 180 | 181 | // Check if the module balance is sufficient to approve 182 | moduleTokenBalance := moduleInfo.GetUserTokenBalance(approveInfo.Tick, data.PkScript) 183 | // available > amt 184 | if moduleTokenBalance.AvailableBalance.Cmp(balanceApprove) < 0 { // invalid 185 | history.Valid = false 186 | g.InscriptionsInvalidApproveMap[data.CreateIdxKey] = approveInfo 187 | } else { 188 | history.Valid = true 189 | // The available balance here needs to be directly deducted and transferred to ApproveableBalance. 190 | moduleTokenBalance.AvailableBalanceSafe = moduleTokenBalance.AvailableBalanceSafe.Sub(balanceApprove) 191 | moduleTokenBalance.AvailableBalance = moduleTokenBalance.AvailableBalance.Sub(balanceApprove) 192 | moduleTokenBalance.ApproveableBalance = moduleTokenBalance.ApproveableBalance.Add(balanceApprove) 193 | 194 | // Update personal approve lookup table ValidApproveMap 195 | if moduleTokenBalance.ValidApproveMap == nil { 196 | moduleTokenBalance.ValidApproveMap = make(map[string]*model.InscriptionBRC20Data, 1) 197 | } 198 | moduleTokenBalance.ValidApproveMap[data.CreateIdxKey] = data 199 | 200 | moduleTokenBalance.UpdateHeight = g.BestHeight 201 | // Update global approve lookup table 202 | g.InscriptionsValidApproveMap[data.CreateIdxKey] = approveInfo 203 | 204 | // g.InscriptionsValidBRC20DataMap[data.CreateIdxKey] = approveInfo.Data // fixme 205 | } 206 | 207 | return nil 208 | } 209 | -------------------------------------------------------------------------------- /indexer/module_commit.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | 9 | "github.com/unisat-wallet/libbrc20-indexer/constant" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func (g *BRC20ModuleIndexer) GetCommitInfoByKey(createIdxKey string) ( 15 | commitData *model.InscriptionBRC20Data, isInvalid bool) { 16 | var ok bool 17 | // commit 18 | commitData, ok = g.InscriptionsValidCommitMap[createIdxKey] 19 | if !ok { 20 | commitData, ok = g.InscriptionsInvalidCommitMap[createIdxKey] 21 | if !ok { 22 | commitData = nil 23 | } 24 | isInvalid = true 25 | } 26 | 27 | return commitData, isInvalid 28 | } 29 | 30 | func (g *BRC20ModuleIndexer) ProcessCommit(dataFrom, dataTo *model.InscriptionBRC20Data, isInvalid bool) error { 31 | inscriptionId := dataFrom.GetInscriptionId() 32 | log.Printf("parse move commit. inscription id: %s", inscriptionId) 33 | 34 | // Delete the already sent commit 35 | delete(g.InscriptionsValidCommitMapById, inscriptionId) 36 | 37 | var body *model.InscriptionBRC20ModuleSwapCommitContent 38 | if err := json.Unmarshal(dataFrom.ContentBody, &body); err != nil { 39 | log.Printf("parse module commit json failed. txid: %s", 40 | hex.EncodeToString(utils.ReverseBytes([]byte(dataTo.TxId))), 41 | ) 42 | return errors.New("json") 43 | } 44 | 45 | // Check the inscription reception address, it must be a module address. 46 | moduleId, ok := utils.GetModuleFromScript([]byte(dataTo.PkScript)) 47 | if !ok || moduleId != body.Module { 48 | return errors.New("commit, not send to module") 49 | } 50 | 51 | // check module exist 52 | moduleInfo, ok := g.ModulesInfoMap[body.Module] 53 | if !ok { 54 | return errors.New("commit, module not exist") 55 | } 56 | 57 | // preset invalid 58 | moduleInfo.CommitInvalidMap[inscriptionId] = struct{}{} 59 | 60 | // Check the inscription sending address, it must be the sequencer address. 61 | if moduleInfo.SequencerPkScript != dataFrom.PkScript { 62 | return errors.New("module sequencer invalid") 63 | } 64 | 65 | eachFuntionSize, err := GetEachItemLengthOfCommitJsonData(dataFrom.ContentBody) 66 | if err != nil || len(body.Data) != len(eachFuntionSize) { 67 | return errors.New("commit, get function size failed") 68 | } 69 | 70 | log.Printf("ProcessCommitVerify commit[%s] ", inscriptionId) 71 | var pickUsersPkScript = make(map[string]bool, 0) 72 | var pickTokensTick = make(map[string]bool, 0) 73 | var pickPoolsPair = make(map[string]bool, 0) 74 | g.InitCherryPickFilter(body, pickUsersPkScript, pickTokensTick, pickPoolsPair) 75 | swapState := g.CherryPick(body.Module, pickUsersPkScript, pickTokensTick, pickPoolsPair) 76 | 77 | // Need to cherrypick, then verify on the copy. 78 | if idx, _, err := swapState.ProcessCommitVerify(inscriptionId, body, eachFuntionSize, nil); err != nil { 79 | log.Printf("commit invalid, function[%d] %s, txid: %s", idx, err, hex.EncodeToString([]byte(dataTo.TxId))) 80 | return err 81 | } 82 | // Execute in reality if successful. 83 | if idx, _, err := g.ProcessCommitVerify(inscriptionId, body, eachFuntionSize, nil); err != nil { 84 | log.Printf("commit invalid, function[%d] %s, txid: %s", idx, err, hex.EncodeToString([]byte(dataTo.TxId))) 85 | return err 86 | } 87 | 88 | // set commit id 89 | moduleInfo.CommitIdMap[inscriptionId] = struct{}{} 90 | moduleInfo.CommitIdChainMap[body.Parent] = struct{}{} 91 | 92 | // valid 93 | delete(moduleInfo.CommitInvalidMap, inscriptionId) 94 | 95 | history := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_COMMIT, dataFrom, dataTo, nil, true) 96 | moduleInfo.History = append(moduleInfo.History, history) 97 | return nil 98 | } 99 | 100 | func GetCommitParentFromData(data *model.InscriptionBRC20Data) (string, error) { 101 | var body *model.InscriptionBRC20ModuleSwapCommitContent 102 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 103 | return "", errors.New("json") 104 | } 105 | return body.Parent, nil 106 | } 107 | 108 | func (g *BRC20ModuleIndexer) ProcessCommitCheck(data *model.InscriptionBRC20Data) (int, error) { 109 | var body *model.InscriptionBRC20ModuleSwapCommitContent 110 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 111 | return -1, errors.New("json") 112 | } 113 | 114 | // check module exist 115 | moduleInfo, ok := g.ModulesInfoMap[body.Module] 116 | if !ok { 117 | return -1, errors.New("commit, module not exist") 118 | } 119 | 120 | eachFuntionSize, err := GetEachItemLengthOfCommitJsonData(data.ContentBody) 121 | if err != nil || len(body.Data) != len(eachFuntionSize) { 122 | return -1, errors.New("commit, get function size failed") 123 | } 124 | 125 | inscriptionId := data.GetInscriptionId() 126 | log.Printf("ProcessCommitVerify commit[%s] ", inscriptionId) 127 | idx, _, err := g.ProcessCommitVerify(inscriptionId, body, eachFuntionSize, nil) 128 | if err != nil { 129 | return idx, err 130 | } 131 | 132 | // set commit id 133 | moduleInfo.CommitIdMap[inscriptionId] = struct{}{} 134 | moduleInfo.CommitIdChainMap[body.Parent] = struct{}{} 135 | 136 | // Delete the already sent commit 137 | delete(g.InscriptionsValidCommitMapById, inscriptionId) 138 | 139 | return 0, nil 140 | } 141 | -------------------------------------------------------------------------------- /indexer/module_commit_add_liq.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/unisat-wallet/libbrc20-indexer/conf" 8 | "github.com/unisat-wallet/libbrc20-indexer/constant" 9 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionAddLiquidity(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) (err error) { 15 | token0, token1 := f.Params[0], f.Params[1] 16 | if g.BestHeight < conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 17 | token0, token1, err = utils.DecodeTokensFromSwapPair(f.Params[0]) 18 | if err != nil { 19 | return errors.New("func: addLiq poolPair invalid") 20 | } 21 | } 22 | 23 | poolPair := GetLowerInnerPairNameByToken(token0, token1) 24 | pool, ok := moduleInfo.SwapPoolTotalBalanceDataMap[poolPair] 25 | if !ok { 26 | return errors.New("addLiq: pool invalid") 27 | } 28 | 29 | usersLpBalanceInPool, ok := moduleInfo.LPTokenUsersBalanceMap[poolPair] 30 | if !ok { 31 | return errors.New("addLiq: users invalid") 32 | } 33 | 34 | // log.Printf("[%s] pool before addliq [%s] %s: %s, %s: %s, lp: %s", moduleInfo.ID, poolPair, pool.Tick[0], pool.TickBalance[0], pool.Tick[1], pool.TickBalance[1], pool.LpBalance) 35 | log.Printf("pool addliq params: %v", f.Params) 36 | 37 | offset := 0 38 | if g.BestHeight >= conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 39 | offset = 1 40 | } 41 | token0AmtStr := f.Params[1+offset] 42 | token1AmtStr := f.Params[2+offset] 43 | tokenLpAmtStr := f.Params[3+offset] 44 | 45 | token0Amt, _ := g.CheckTickVerify(token0, token0AmtStr) 46 | token1Amt, _ := g.CheckTickVerify(token1, token1AmtStr) 47 | tokenLpAmt, _ := decimal.NewDecimalFromString(tokenLpAmtStr, 18) 48 | 49 | // LP Balance Slippage Check 50 | slippageAmtStr := f.Params[4+offset] 51 | slippageAmt, _ := decimal.NewDecimalFromString(slippageAmtStr, 3) 52 | 53 | var token0Idx, token1Idx int 54 | if token0 == pool.Tick[0] { 55 | token0Idx = 0 56 | token1Idx = 1 57 | } else { 58 | token0Idx = 1 59 | token1Idx = 0 60 | } 61 | 62 | var first bool = false 63 | var lpForPool, lpForUser *decimal.Decimal 64 | if pool.TickBalance[0].Sign() == 0 && pool.TickBalance[1].Sign() == 0 { 65 | first = true 66 | lpForPool = token0Amt.Mul(token1Amt).Sqrt() 67 | if lpForPool.Cmp(decimal.NewDecimal(1000, 18)) < 0 { 68 | return errors.New("addLiq: lp less than 1000") 69 | } 70 | lpForUser = lpForPool.Sub(decimal.NewDecimal(1000, 18)) 71 | 72 | } else { 73 | // Issuing additional LP, as a way of collecting service fees. 74 | feeRateSwapAmt, ok := CheckAmountVerify(moduleInfo.FeeRateSwap, 3) 75 | if !ok { 76 | log.Printf("pool addliq FeeRateSwap invalid: %s", moduleInfo.FeeRateSwap) 77 | return errors.New("addLiq: feerate swap invalid") 78 | } 79 | if feeRateSwapAmt.Sign() > 0 { 80 | // lp = (poolLp * (rootK - rootKLast)) / (rootK * 5 + rootKLast) 81 | rootK := pool.TickBalance[token0Idx].Mul(pool.TickBalance[token1Idx]).Sqrt() 82 | 83 | lpFee := pool.LpBalance.Mul(rootK.Sub(pool.LastRootK)).Div( 84 | rootK.Mul(decimal.NewDecimal(5, 0)).Add(pool.LastRootK)) 85 | 86 | log.Printf("pool addliq issue lp: %s", lpFee.String()) 87 | if lpFee.Sign() > 0 { 88 | // pool lp update 89 | pool.LpBalance = pool.LpBalance.Add(lpFee) 90 | 91 | // lpFee lp balance update 92 | lpFeelpbalance := usersLpBalanceInPool[moduleInfo.LpFeePkScript] 93 | lpFeelpbalance = lpFeelpbalance.Add(lpFee) 94 | usersLpBalanceInPool[moduleInfo.LpFeePkScript] = lpFeelpbalance 95 | // set update flag 96 | moduleInfo.LPTokenUsersBalanceUpdatedMap[poolPair+moduleInfo.LpFeePkScript] = struct{}{} 97 | // lpFee-lp-balance 98 | lpFeelpsBalance, ok := moduleInfo.UsersLPTokenBalanceMap[moduleInfo.LpFeePkScript] 99 | if !ok { 100 | lpFeelpsBalance = make(map[string]*decimal.Decimal, 0) 101 | moduleInfo.UsersLPTokenBalanceMap[moduleInfo.LpFeePkScript] = lpFeelpsBalance 102 | } 103 | lpFeelpsBalance[poolPair] = lpFeelpbalance 104 | } 105 | } 106 | 107 | // Calculate the amount of liquidity tokens acquired 108 | token1AdjustAmt := pool.TickBalance[token1Idx].Mul(token0Amt).Div(pool.TickBalance[token0Idx]) 109 | if token1Amt.Cmp(token1AdjustAmt) >= 0 { 110 | token1Amt = token1AdjustAmt 111 | } else { 112 | token0AdjustAmt := pool.TickBalance[token0Idx].Mul(token1Amt).Div(pool.TickBalance[token1Idx]) 113 | token0Amt = token0AdjustAmt 114 | } 115 | 116 | lp0 := pool.LpBalance.Mul(token0Amt).Div(pool.TickBalance[token0Idx]) 117 | lp1 := pool.LpBalance.Mul(token1Amt).Div(pool.TickBalance[token1Idx]) 118 | if lp0.Cmp(lp1) > 0 { 119 | lpForPool = lp1 120 | } else { 121 | lpForPool = lp0 122 | } 123 | lpForUser = lpForPool 124 | } 125 | 126 | if lpForUser.Cmp(tokenLpAmt.Mul(decimal.NewDecimal(1000, 3).Sub(slippageAmt)).Div(decimal.NewDecimal(1000, 3))) < 0 { 127 | log.Printf("user[%s], lp: %s < expect: %s. * %s", f.Address, lpForUser, tokenLpAmt, tokenLpAmt.Sub(tokenLpAmt.Mul(slippageAmt))) 128 | return errors.New("addLiq: over slippage") 129 | } 130 | 131 | // User Balance Check 132 | token0Balance := moduleInfo.GetUserTokenBalance(token0, f.PkScript) 133 | token1Balance := moduleInfo.GetUserTokenBalance(token1, f.PkScript) 134 | // fixme: Must use the confirmed amount 135 | if token0Balance.SwapAccountBalance.Cmp(token0Amt) < 0 { 136 | log.Printf("token0[%s] user[%s], balance %s", token0, f.Address, token0Balance) 137 | return errors.New("addLiq: token0 balance insufficient") 138 | } 139 | // fixme: Must use the confirmed amount 140 | if token1Balance.SwapAccountBalance.Cmp(token1Amt) < 0 { 141 | log.Printf("token1[%s] user[%s], balance %s", token1, f.Address, token1Balance) 142 | return errors.New("addLiq: token1 balance insufficient") 143 | } 144 | 145 | // User Real-time Balance Update 146 | token0Balance.SwapAccountBalance = token0Balance.SwapAccountBalance.Sub(token0Amt) 147 | token1Balance.SwapAccountBalance = token1Balance.SwapAccountBalance.Sub(token1Amt) 148 | 149 | token0Balance.UpdateHeight = g.BestHeight 150 | token1Balance.UpdateHeight = g.BestHeight 151 | 152 | // fixme: User safety balance update 153 | 154 | // lp balance update 155 | // lp-user-balance 156 | lpbalance := usersLpBalanceInPool[f.PkScript] 157 | lpbalance = lpbalance.Add(lpForUser) 158 | usersLpBalanceInPool[f.PkScript] = lpbalance 159 | // user-lp-balance 160 | lpsBalance, ok := moduleInfo.UsersLPTokenBalanceMap[f.PkScript] 161 | if !ok { 162 | lpsBalance = make(map[string]*decimal.Decimal, 0) 163 | moduleInfo.UsersLPTokenBalanceMap[f.PkScript] = lpsBalance 164 | } 165 | lpsBalance[poolPair] = lpbalance 166 | 167 | // zero address lp balance update 168 | if first { 169 | zerolpbalance := usersLpBalanceInPool[constant.ZERO_ADDRESS_PKSCRIPT] 170 | zerolpbalance = zerolpbalance.Add(decimal.NewDecimal(1000, 18)) 171 | usersLpBalanceInPool[constant.ZERO_ADDRESS_PKSCRIPT] = zerolpbalance 172 | // zerouser-lp-balance 173 | zerolpsBalance, ok := moduleInfo.UsersLPTokenBalanceMap[constant.ZERO_ADDRESS_PKSCRIPT] 174 | if !ok { 175 | zerolpsBalance = make(map[string]*decimal.Decimal, 0) 176 | moduleInfo.UsersLPTokenBalanceMap[constant.ZERO_ADDRESS_PKSCRIPT] = zerolpsBalance 177 | } 178 | zerolpsBalance[poolPair] = zerolpbalance 179 | } 180 | 181 | // Changes in pool balance 182 | pool.TickBalance[token0Idx] = pool.TickBalance[token0Idx].Add(token0Amt) 183 | pool.TickBalance[token1Idx] = pool.TickBalance[token1Idx].Add(token1Amt) 184 | pool.LpBalance = pool.LpBalance.Add(lpForPool) 185 | 186 | pool.UpdateHeight = g.BestHeight 187 | 188 | // update lastRootK 189 | pool.LastRootK = pool.TickBalance[token0Idx].Mul(pool.TickBalance[token1Idx]).Sqrt() 190 | 191 | // log.Printf("[%s] pool after addliq [%s] %s: %s, %s: %s, lp: %s", moduleInfo.ID, poolPair, pool.Tick[0], pool.TickBalance[0], pool.Tick[1], pool.TickBalance[1], pool.LpBalance) 192 | return nil 193 | } 194 | -------------------------------------------------------------------------------- /indexer/module_commit_decrease_approval.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/unisat-wallet/libbrc20-indexer/model" 8 | ) 9 | 10 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionDecreaseApproval(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) error { 11 | 12 | token := f.Params[0] 13 | tokenAmtStr := f.Params[1] 14 | tokenAmt, _ := g.CheckTickVerify(token, tokenAmtStr) 15 | 16 | tokenBalance := moduleInfo.GetUserTokenBalance(token, f.PkScript) 17 | 18 | // fixme: Must use the confirmed amount 19 | if tokenBalance.SwapAccountBalance.Cmp(tokenAmt) < 0 { 20 | log.Printf("token[%s] user[%s], balance %s", token, f.Address, tokenBalance) 21 | return errors.New("decreaseApproval: token balance insufficient") 22 | } 23 | 24 | // User Real-time Balance Update 25 | tokenBalance.SwapAccountBalance = tokenBalance.SwapAccountBalance.Sub(tokenAmt) 26 | tokenBalance.AvailableBalance = tokenBalance.AvailableBalance.Add(tokenAmt) 27 | 28 | tokenBalance.UpdateHeight = g.BestHeight 29 | 30 | log.Printf("pool decreaseApproval [%s] available: %s, swappable: %s", token, tokenBalance.AvailableBalance, tokenBalance.SwapAccountBalance) 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /indexer/module_commit_deploy_pool.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 8 | "github.com/unisat-wallet/libbrc20-indexer/model" 9 | ) 10 | 11 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionDeployPool(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) error { 12 | token0, token1 := f.Params[0], f.Params[1] 13 | poolPair := GetLowerInnerPairNameByToken(token0, token1) 14 | if _, ok := moduleInfo.SwapPoolTotalBalanceDataMap[poolPair]; ok { 15 | return errors.New("deploy: twice") 16 | } 17 | 18 | // lp token balance of address in module [pool][address]balance 19 | moduleInfo.LPTokenUsersBalanceMap[poolPair] = make(map[string]*decimal.Decimal, 0) 20 | 21 | token0Amt, _ := g.CheckTickVerify(token0, "0") 22 | token1Amt, _ := g.CheckTickVerify(token1, "0") 23 | 24 | // swap total balance 25 | // total balance of pool in module [pool]balanceData 26 | moduleInfo.SwapPoolTotalBalanceDataMap[poolPair] = &model.BRC20ModulePoolTotalBalance{ 27 | Tick: [2]string{token0, token1}, 28 | History: make([]*model.BRC20ModuleHistory, 0), // fixme: 29 | // balance 30 | TickBalance: [2]*decimal.Decimal{token0Amt, token1Amt}, 31 | } 32 | log.Printf("[%s] pool deploy pool [%s]", moduleInfo.ID, poolPair) 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /indexer/module_commit_gas.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "log" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/conf" 9 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionGasFee(moduleInfo *model.BRC20ModuleSwapInfo, userPkScript string, gasAmt *decimal.Decimal) error { 15 | 16 | tokenBalance := moduleInfo.GetUserTokenBalance(moduleInfo.GasTick, userPkScript) 17 | // fixme: Must use the confirmed amount 18 | if tokenBalance.SwapAccountBalance.Cmp(gasAmt) < 0 { 19 | address, err := utils.GetAddressFromScript([]byte(userPkScript), conf.GlobalNetParams) 20 | if err != nil { 21 | address = hex.EncodeToString([]byte(userPkScript)) 22 | } 23 | 24 | log.Printf("gas[%s] user[%s], balance %s", moduleInfo.GasTick, address, tokenBalance) 25 | return errors.New("gas fee: token balance insufficient") 26 | } 27 | 28 | gasToBalance := moduleInfo.GetUserTokenBalance(moduleInfo.GasTick, moduleInfo.GasToPkScript) 29 | 30 | // User Real-time gas Balance Update 31 | tokenBalance.SwapAccountBalance = tokenBalance.SwapAccountBalance.Sub(gasAmt) 32 | gasToBalance.SwapAccountBalance = gasToBalance.SwapAccountBalance.Add(gasAmt) 33 | 34 | tokenBalance.UpdateHeight = g.BestHeight 35 | gasToBalance.UpdateHeight = g.BestHeight 36 | 37 | // log.Printf("gas fee[%s]: %s user: %s, gasTo: %s", moduleInfo.GasTick, gasAmt, tokenBalance.SwapAccountBalance, gasToBalance.SwapAccountBalance) 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /indexer/module_commit_inscribe.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | "strings" 9 | 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | // ProcessInscribeCommit inscribed a commit, but it has not taken effect yet. 15 | func (g *BRC20ModuleIndexer) ProcessInscribeCommit(data *model.InscriptionBRC20Data) (err error) { 16 | inscriptionId := data.GetInscriptionId() 17 | log.Printf("parse new inscribe commit. inscription id: %s", inscriptionId) 18 | 19 | var body *model.InscriptionBRC20ModuleSwapCommitContent 20 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 21 | log.Printf("parse commit json failed. txid: %s", 22 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 23 | ) 24 | return errors.New("json invalid") 25 | } 26 | 27 | // lower case module id only 28 | if body.Module != strings.ToLower(body.Module) { 29 | return errors.New("module id invalid") 30 | } 31 | 32 | // check module exist 33 | moduleInfo, ok := g.ModulesInfoMap[body.Module] 34 | if !ok { 35 | return errors.New("module invalid") 36 | } 37 | 38 | // preset invalid 39 | moduleInfo.CommitInvalidMap[inscriptionId] = struct{}{} 40 | 41 | // check sequencer match 42 | if moduleInfo.SequencerPkScript != data.PkScript { 43 | return errors.New("module sequencer invalid") 44 | } 45 | 46 | idx, err := g.ProcessInscribeCommitPreVerify(body) 47 | if err != nil { 48 | log.Printf("commit invalid inscribe. function[%d], %s, txid: %s", idx, err, hex.EncodeToString([]byte(data.TxId))) 49 | return err 50 | } 51 | g.InscriptionsValidCommitMap[data.CreateIdxKey] = data 52 | g.InscriptionsValidCommitMapById[inscriptionId] = data 53 | 54 | // valid 55 | delete(moduleInfo.CommitInvalidMap, inscriptionId) 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /indexer/module_commit_remove_liq.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/conf" 9 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionRemoveLiquidity(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) (err error) { 15 | token0, token1 := f.Params[0], f.Params[1] 16 | if g.BestHeight < conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 17 | token0, token1, err = utils.DecodeTokensFromSwapPair(f.Params[0]) 18 | if err != nil { 19 | return errors.New("func: removeLiq poolPair invalid") 20 | } 21 | } 22 | poolPair := GetLowerInnerPairNameByToken(token0, token1) 23 | pool, ok := moduleInfo.SwapPoolTotalBalanceDataMap[poolPair] 24 | if !ok { 25 | return errors.New("removeLiq: pool invalid") 26 | } 27 | usersLpBalanceInPool, ok := moduleInfo.LPTokenUsersBalanceMap[poolPair] 28 | if !ok { 29 | return errors.New("removeLiq: lps balance map missing pair") 30 | } 31 | lpsBalance, ok := moduleInfo.UsersLPTokenBalanceMap[f.PkScript] 32 | if !ok { 33 | return errors.New("removeLiq: users balance map missing user") 34 | } 35 | 36 | // log.Printf("[%s] pool before removeliq [%s] %s: %s, %s: %s, lp: %s", moduleInfo.ID, poolPair, pool.Tick[0], pool.TickBalance[0], pool.Tick[1], pool.TickBalance[1], pool.LpBalance) 37 | log.Printf("pool removeliq params: %v", f.Params) 38 | 39 | offset := 0 40 | if g.BestHeight >= conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 41 | offset = 1 42 | } 43 | 44 | tokenLpAmtStr := f.Params[1+offset] 45 | token0AmtStr := f.Params[2+offset] 46 | token1AmtStr := f.Params[3+offset] 47 | 48 | token0Amt, _ := g.CheckTickVerify(token0, token0AmtStr) 49 | token1Amt, _ := g.CheckTickVerify(token1, token1AmtStr) 50 | tokenLpAmt, _ := decimal.NewDecimalFromString(tokenLpAmtStr, 18) 51 | 52 | // LP Balance Slippage Check 53 | slippageAmtStr := f.Params[4+offset] 54 | slippageAmt, _ := decimal.NewDecimalFromString(slippageAmtStr, 3) 55 | 56 | var token0Idx, token1Idx int 57 | if token0 == pool.Tick[0] { 58 | token0Idx = 0 59 | token1Idx = 1 60 | } else { 61 | token0Idx = 1 62 | token1Idx = 0 63 | } 64 | 65 | // Increase LP, as a method of collecting service fees. 66 | feeRateSwapAmt, _ := CheckAmountVerify(moduleInfo.FeeRateSwap, 3) 67 | if feeRateSwapAmt.Sign() > 0 { 68 | // lp = (poolLp * (rootK - rootKLast)) / (rootK * 5 + rootKLast) 69 | rootK := pool.TickBalance[token0Idx].Mul(pool.TickBalance[token1Idx]).Sqrt() 70 | 71 | lpFee := pool.LpBalance.Mul(rootK.Sub(pool.LastRootK)).Div( 72 | rootK.Mul(decimal.NewDecimal(5, 0)).Add(pool.LastRootK)) 73 | if lpFee.Sign() > 0 { 74 | // pool lp update 75 | pool.LpBalance = pool.LpBalance.Add(lpFee) 76 | 77 | // lpFee update 78 | lpFeelpbalance := usersLpBalanceInPool[moduleInfo.LpFeePkScript] 79 | lpFeelpbalance = lpFeelpbalance.Add(lpFee) 80 | usersLpBalanceInPool[moduleInfo.LpFeePkScript] = lpFeelpbalance 81 | // set update flag 82 | moduleInfo.LPTokenUsersBalanceUpdatedMap[poolPair+moduleInfo.LpFeePkScript] = struct{}{} 83 | // lpFee-lp-balance 84 | lpFeelpsBalance, ok := moduleInfo.UsersLPTokenBalanceMap[moduleInfo.LpFeePkScript] 85 | if !ok { 86 | lpFeelpsBalance = make(map[string]*decimal.Decimal, 0) 87 | moduleInfo.UsersLPTokenBalanceMap[moduleInfo.LpFeePkScript] = lpFeelpsBalance 88 | } 89 | lpFeelpsBalance[poolPair] = lpFeelpbalance 90 | } 91 | } 92 | 93 | // Slippage Check 94 | amt0 := pool.TickBalance[token0Idx].Mul(tokenLpAmt).Div(pool.LpBalance) 95 | if amt0.Cmp(token0Amt.Sub(token0Amt.Mul(slippageAmt))) < 0 { 96 | log.Printf("user[%s], token0: %s, expect: %s", f.Address, amt0, token0Amt) 97 | return errors.New("removeLiq: over slippage") 98 | } 99 | amt1 := pool.TickBalance[token1Idx].Mul(tokenLpAmt).Div(pool.LpBalance) 100 | if amt1.Cmp(token1Amt.Sub(token1Amt.Mul(slippageAmt))) < 0 { 101 | log.Printf("user[%s], token1: %s, expect: %s", f.Address, amt1, token1Amt) 102 | return errors.New("removeLiq: over slippage") 103 | } 104 | 105 | // Changes in pool balance 106 | if pool.LpBalance.Cmp(tokenLpAmt) < 0 { 107 | return errors.New(fmt.Sprintf("removeLiq: tokenLp balance insufficient, %s < %s", pool.LpBalance, tokenLpAmt)) 108 | } 109 | if pool.TickBalance[token0Idx].Cmp(amt0) < 0 { 110 | return errors.New(fmt.Sprintf("removeLiq: pool %s balance insufficient", pool.Tick[token1Idx])) 111 | } 112 | if pool.TickBalance[token1Idx].Cmp(amt1) < 0 { 113 | return errors.New(fmt.Sprintf("removeLiq: pool %s balance insufficient", pool.Tick[token1Idx])) 114 | } 115 | 116 | // Check whether the user's LP balance is consistent (consider storing only one copy) 117 | userbalance := usersLpBalanceInPool[f.PkScript] 118 | lpBalance := lpsBalance[poolPair] 119 | if userbalance.Cmp(lpBalance) != 0 { 120 | return errors.New("removeLiq: user's tokenLp balance miss match") 121 | } 122 | // Check whether the balance of user LP is sufficient. 123 | if userbalance.Cmp(tokenLpAmt) < 0 { 124 | return errors.New(fmt.Sprintf("removeLiq: user's tokenLp balance insufficient, %s < %s", userbalance, tokenLpAmt)) 125 | } 126 | if lpBalance.Cmp(tokenLpAmt) < 0 { 127 | return errors.New(fmt.Sprintf("removeLiq: user's tokenLp balance insufficient, %s < %s", lpBalance, tokenLpAmt)) 128 | } 129 | 130 | // update lp balance 131 | usersLpBalanceInPool[f.PkScript] = userbalance.Sub(tokenLpAmt) 132 | lpsBalance[poolPair] = lpBalance.Sub(tokenLpAmt) 133 | 134 | token0Balance := moduleInfo.GetUserTokenBalance(token0, f.PkScript) 135 | token1Balance := moduleInfo.GetUserTokenBalance(token1, f.PkScript) 136 | 137 | // Obtains user token balance 138 | token0Balance.SwapAccountBalance = token0Balance.SwapAccountBalance.Add(amt0) 139 | token1Balance.SwapAccountBalance = token1Balance.SwapAccountBalance.Add(amt1) 140 | 141 | // update at height 142 | token0Balance.UpdateHeight = g.BestHeight 143 | token1Balance.UpdateHeight = g.BestHeight 144 | pool.UpdateHeight = g.BestHeight 145 | 146 | pool.LpBalance = pool.LpBalance.Sub(tokenLpAmt) // fixme 147 | 148 | // Deduct token balance in the pool 149 | pool.TickBalance[token0Idx] = pool.TickBalance[token0Idx].Sub(amt0) 150 | pool.TickBalance[token1Idx] = pool.TickBalance[token1Idx].Sub(amt1) 151 | 152 | // update lastRootK 153 | pool.LastRootK = pool.TickBalance[token0Idx].Mul(pool.TickBalance[token1Idx]).Sqrt() 154 | 155 | // log.Printf("[%s] pool after removeliq [%s] %s: %s, %s: %s, lp: %s", moduleInfo.ID, poolPair, pool.Tick[0], pool.TickBalance[0], pool.Tick[1], pool.TickBalance[1], pool.LpBalance) 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /indexer/module_commit_send.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/unisat-wallet/libbrc20-indexer/conf" 8 | "github.com/unisat-wallet/libbrc20-indexer/model" 9 | "github.com/unisat-wallet/libbrc20-indexer/utils" 10 | ) 11 | 12 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionSend(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) error { 13 | addressTo := f.Params[0] 14 | pkScriptTo, _ := utils.GetPkScriptByAddress(addressTo, conf.GlobalNetParams) 15 | 16 | token := f.Params[1] 17 | tokenAmtStr := f.Params[2] 18 | 19 | tokenAmt, _ := g.CheckTickVerify(token, tokenAmtStr) 20 | tokenBalanceFrom := moduleInfo.GetUserTokenBalance(token, f.PkScript) 21 | 22 | // fixme: Must use the confirmed amount 23 | if tokenBalanceFrom.SwapAccountBalance.Cmp(tokenAmt) < 0 { 24 | log.Printf("token[%s] user[%s], balance %s", token, f.Address, tokenBalanceFrom) 25 | return errors.New("send: token balance insufficient") 26 | } 27 | 28 | tokenBalanceTo := moduleInfo.GetUserTokenBalance(token, string(pkScriptTo)) 29 | 30 | // User Real-time Balance Update 31 | tokenBalanceFrom.SwapAccountBalance = tokenBalanceFrom.SwapAccountBalance.Sub(tokenAmt) 32 | tokenBalanceTo.SwapAccountBalance = tokenBalanceTo.SwapAccountBalance.Add(tokenAmt) 33 | 34 | tokenBalanceFrom.UpdateHeight = g.BestHeight 35 | tokenBalanceTo.UpdateHeight = g.BestHeight 36 | 37 | log.Printf("pool send [%s] swappable: %s -> %s", token, tokenBalanceFrom.SwapAccountBalance, tokenBalanceTo.SwapAccountBalance) 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /indexer/module_commit_sendlp.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/conf" 9 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionSendLp(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) error { 15 | addressTo := f.Params[0] 16 | pkScriptTo, _ := utils.GetPkScriptByAddress(addressTo, conf.GlobalNetParams) 17 | 18 | token0, token1 := f.Params[1], f.Params[2] 19 | poolPair := GetLowerInnerPairNameByToken(token0, token1) 20 | if _, ok := moduleInfo.SwapPoolTotalBalanceDataMap[poolPair]; !ok { 21 | return errors.New("sendlp: pool invalid") 22 | } 23 | usersLpBalanceInPool, ok := moduleInfo.LPTokenUsersBalanceMap[poolPair] 24 | if !ok { 25 | return errors.New("sendlp: lps balance map missing pair") 26 | } 27 | 28 | // Check whether the lp user's balance storage is consistent (consider storing only one copy) 29 | lpsBalanceFrom, ok := moduleInfo.UsersLPTokenBalanceMap[f.PkScript] 30 | if !ok { 31 | return errors.New("sendlp: users balance map missing user") 32 | } 33 | lpBalanceFrom := lpsBalanceFrom[poolPair] 34 | 35 | userbalanceFrom := usersLpBalanceInPool[f.PkScript] 36 | if userbalanceFrom.Cmp(lpBalanceFrom) != 0 { 37 | return errors.New("sendlp: user's tokenLp balance miss match") 38 | } 39 | 40 | tokenAmtStr := f.Params[3] 41 | tokenLpAmt, _ := CheckAmountVerify(tokenAmtStr, 18) 42 | // Check if the user's lp balance is sufficient. 43 | if userbalanceFrom.Cmp(tokenLpAmt) < 0 { 44 | return errors.New(fmt.Sprintf("sendlp: user's tokenLp balance insufficient, %s < %s", userbalanceFrom, tokenLpAmt)) 45 | } 46 | if lpBalanceFrom.Cmp(tokenLpAmt) < 0 { 47 | return errors.New(fmt.Sprintf("sendlp: user's tokenLp balance insufficient, %s < %s", lpBalanceFrom, tokenLpAmt)) 48 | } 49 | 50 | // update from lp balance 51 | usersLpBalanceInPool[f.PkScript] = userbalanceFrom.Sub(tokenLpAmt) 52 | lpsBalanceFrom[poolPair] = lpBalanceFrom.Sub(tokenLpAmt) 53 | 54 | // update to lp balance 55 | lpBalanceTo := usersLpBalanceInPool[string(pkScriptTo)] 56 | lpBalanceTo = lpBalanceTo.Add(tokenLpAmt) 57 | usersLpBalanceInPool[string(pkScriptTo)] = lpBalanceTo 58 | 59 | // set update flag 60 | moduleInfo.LPTokenUsersBalanceUpdatedMap[poolPair+f.PkScript] = struct{}{} 61 | moduleInfo.LPTokenUsersBalanceUpdatedMap[poolPair+string(pkScriptTo)] = struct{}{} 62 | 63 | // touser-lp-balance 64 | lpsBalanceTo, ok := moduleInfo.UsersLPTokenBalanceMap[string(pkScriptTo)] 65 | if !ok { 66 | lpsBalanceTo = make(map[string]*decimal.Decimal, 0) 67 | moduleInfo.UsersLPTokenBalanceMap[string(pkScriptTo)] = lpsBalanceTo 68 | } 69 | lpsBalanceTo[poolPair] = lpBalanceTo 70 | 71 | log.Printf("pool sendlp [%s] lp: %s -> %s", poolPair, lpBalanceFrom, lpBalanceTo) 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /indexer/module_commit_swap.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/conf" 9 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 10 | "github.com/unisat-wallet/libbrc20-indexer/model" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | // ProcessCommitFunctionSwap 15 | // exactIn: 16 | // 17 | // amountInWithFee = amountIn * 997 18 | // amountOut = (amountInWithFee * reserveOut)/(reverseIn * 1000 + amountInWithFee) 19 | // 20 | // exactOut: 21 | // 22 | // amountIn = (reserveIn * amountOut * 1000)/((reserveOut - amountOut) * 997) + 1 23 | func (g *BRC20ModuleIndexer) ProcessCommitFunctionSwap(moduleInfo *model.BRC20ModuleSwapInfo, f *model.SwapFunctionData) (err error) { 24 | token0, token1 := f.Params[0], f.Params[1] 25 | if g.BestHeight < conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 26 | token0, token1, err = utils.DecodeTokensFromSwapPair(f.Params[0]) 27 | if err != nil { 28 | return errors.New("func: swap poolPair invalid") 29 | } 30 | } 31 | poolPair := GetLowerInnerPairNameByToken(token0, token1) 32 | pool, ok := moduleInfo.SwapPoolTotalBalanceDataMap[poolPair] 33 | if !ok { 34 | return errors.New("swap: pool invalid") 35 | } 36 | 37 | if token0 != pool.Tick[0] && token0 != pool.Tick[1] { 38 | return errors.New("func: swap token invalid") 39 | } 40 | if token1 != pool.Tick[0] && token1 != pool.Tick[1] { 41 | return errors.New("func: swap token invalid") 42 | } 43 | 44 | // log.Printf("[%s] pool before swap [%s] %s: %s, %s: %s, lp: %s", moduleInfo.ID, poolPair, pool.Tick[0], pool.TickBalance[0], pool.Tick[1], pool.TickBalance[1], pool.LpBalance) 45 | log.Printf("pool swap params: %v", f.Params) 46 | 47 | offset := 0 48 | if g.BestHeight >= conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 49 | offset = 1 50 | } 51 | 52 | var tokenIn, tokenInAmtStr, tokenOut, tokenOutAmtStr string 53 | derection := f.Params[3+offset] 54 | if derection == "exactIn" { 55 | tokenIn = f.Params[1+offset] 56 | tokenInAmtStr = f.Params[2+offset] 57 | 58 | if tokenIn == token0 { 59 | tokenOut = token1 60 | } else { 61 | tokenOut = token0 62 | } 63 | tokenOutAmtStr = f.Params[4+offset] 64 | } else if derection == "exactOut" { 65 | tokenOut = f.Params[1+offset] 66 | tokenOutAmtStr = f.Params[2+offset] 67 | 68 | if tokenOut == token0 { 69 | tokenIn = token1 70 | } else { 71 | tokenIn = token0 72 | } 73 | tokenInAmtStr = f.Params[4+offset] 74 | } 75 | 76 | tokenInAmt, _ := g.CheckTickVerify(tokenIn, tokenInAmtStr) 77 | tokenOutAmt, _ := g.CheckTickVerify(tokenOut, tokenOutAmtStr) 78 | 79 | // Confirm the token order id of the pool 80 | var tokenInIdx, tokenOutIdx int 81 | if tokenIn == pool.Tick[0] { 82 | tokenInIdx = 0 83 | tokenOutIdx = 1 84 | } else { 85 | tokenInIdx = 1 86 | tokenOutIdx = 0 87 | } 88 | 89 | // Please note that should use integer calculations here. 90 | // support exactIn 91 | // Slippage check 92 | // exactIn: 1/(1+slippage) * quoteAmount 93 | // exactOut: (1+slippage) * quoteAmount 94 | slippageAmtStr := f.Params[5+offset] 95 | slippageAmt, _ := decimal.NewDecimalFromString(slippageAmtStr, 3) 96 | 97 | feeRateSwapAmt, _ := CheckAmountVerify(moduleInfo.FeeRateSwap, 3) 98 | 99 | var amountIn, amountOut *decimal.Decimal 100 | if derection == "exactIn" { 101 | if feeRateSwapAmt.Sign() > 0 { 102 | // with fee 103 | amountInWithFee := tokenInAmt.Mul(decimal.NewDecimal(1000, 3).Sub(feeRateSwapAmt)) 104 | amountOut = pool.TickBalance[tokenOutIdx].Mul(amountInWithFee).Div( 105 | pool.TickBalance[tokenInIdx].Mul(decimal.NewDecimal(1000, 3)).Add(amountInWithFee)) 106 | } else { 107 | amountOut = pool.TickBalance[tokenOutIdx].Mul(tokenInAmt).Div( 108 | pool.TickBalance[tokenInIdx].Add(tokenInAmt)) 109 | } 110 | 111 | amountOutMin := tokenOutAmt.Mul(decimal.NewDecimal(1000, 3)).Div(decimal.NewDecimal(1000, 3).Add(slippageAmt)) 112 | if amountOut.Cmp(amountOutMin) < 0 { 113 | log.Printf("user[%s], amountOut: %s < expect: %s", f.Address, amountOut, amountOutMin) 114 | return errors.New("swap: slippage error") 115 | } 116 | amountIn = tokenInAmt 117 | 118 | } else if derection == "exactOut" { 119 | if feeRateSwapAmt.Sign() > 0 { 120 | // with fee 121 | amountIn = pool.TickBalance[tokenInIdx].Mul(tokenOutAmt.Mul(decimal.NewDecimal(1000, 3))).Div( 122 | pool.TickBalance[tokenOutIdx].Sub(tokenOutAmt).Mul(decimal.NewDecimal(1000, 3).Sub(feeRateSwapAmt))).Add( 123 | decimal.NewDecimal(1, tokenInAmt.Precition)) 124 | } else { 125 | amountIn = pool.TickBalance[tokenInIdx].Mul(tokenOutAmt).Div( 126 | pool.TickBalance[tokenOutIdx].Sub(tokenOutAmt)).Add( 127 | decimal.NewDecimal(1, tokenInAmt.Precition)) 128 | } 129 | amountInMax := tokenInAmt.Mul(decimal.NewDecimal(1000, 3).Add(slippageAmt)) 130 | if amountInMax.Cmp(amountIn) < 0 { 131 | log.Printf("user[%s], amountIn: %s > expect: %s", f.Address, amountIn, amountInMax) 132 | return errors.New("swap: slippage error") 133 | } 134 | amountOut = tokenOutAmt 135 | } 136 | 137 | // Check the balance range, prepare to update. 138 | if pool.TickBalance[tokenOutIdx].Cmp(amountOut) < 0 { 139 | return errors.New("swap: pool tokenOut balance insufficient") 140 | } 141 | 142 | tokenInBalance := moduleInfo.GetUserTokenBalance(tokenIn, f.PkScript) 143 | tokenOutBalance := moduleInfo.GetUserTokenBalance(tokenOut, f.PkScript) 144 | 145 | tokenInBalance.UpdateHeight = g.BestHeight 146 | tokenOutBalance.UpdateHeight = g.BestHeight 147 | 148 | if tokenInBalance.SwapAccountBalance.Cmp(tokenInAmt) < 0 { 149 | return errors.New(fmt.Sprintf("swap[%s]: user tokenIn balance insufficient: %s < %s", 150 | f.ID, 151 | tokenInBalance.SwapAccountBalance, tokenInAmt)) 152 | } 153 | 154 | // update balance 155 | // swap sub 156 | pool.TickBalance[tokenOutIdx] = pool.TickBalance[tokenOutIdx].Sub(amountOut) 157 | tokenInBalance.SwapAccountBalance = tokenInBalance.SwapAccountBalance.Sub(amountIn) 158 | // swap add 159 | pool.TickBalance[tokenInIdx] = pool.TickBalance[tokenInIdx].Add(amountIn) 160 | tokenOutBalance.SwapAccountBalance = tokenOutBalance.SwapAccountBalance.Add(amountOut) 161 | 162 | pool.UpdateHeight = g.BestHeight 163 | 164 | // log.Printf("[%s] pool after swap [%s] %s: %s, %s: %s, lp: %s", moduleInfo.ID, poolPair, pool.Tick[0], pool.TickBalance[0], pool.Tick[1], pool.TickBalance[1], pool.LpBalance) 165 | return nil 166 | } 167 | -------------------------------------------------------------------------------- /indexer/module_commit_utils.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "log" 12 | "strings" 13 | 14 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 15 | "github.com/unisat-wallet/libbrc20-indexer/model" 16 | "github.com/unisat-wallet/libbrc20-indexer/utils" 17 | "github.com/unisat-wallet/libbrc20-indexer/utils/bip322" 18 | 19 | "github.com/btcsuite/btcd/wire" 20 | ) 21 | 22 | // GetFunctionDataId Calculate ID hash, used for signing. 23 | func GetFunctionDataContent(contentPrefix string, data *model.SwapFunctionData) (content string) { 24 | content = contentPrefix + fmt.Sprintf(`addr: %s 25 | func: %s 26 | params: %s 27 | ts: %d 28 | `, data.Address, data.Function, strings.Join(data.Params, " "), data.Timestamp) 29 | return content 30 | } 31 | 32 | func CheckFunctionSigVerify(contentPrefix string, data *model.SwapFunctionData, previous []string) (id string, ok bool) { 33 | if len(previous) != 0 { 34 | contentPrefix += fmt.Sprintf("prevs: %s\n", strings.Join(previous, " ")) 35 | } 36 | 37 | content := GetFunctionDataContent(contentPrefix, data) 38 | // check id 39 | id = utils.HashString(utils.GetSha256([]byte(content))) 40 | message := GetFunctionDataContent(fmt.Sprintf("id: %s\n", id), data) 41 | 42 | signature, err := base64.StdEncoding.DecodeString(data.Signature) 43 | if err != nil { 44 | log.Println("CheckFunctionSigVerify decoding signature:", err) 45 | return id, false 46 | } 47 | 48 | var wit wire.TxWitness 49 | lenSignature := len(signature) 50 | if len(signature) == 66 { 51 | wit = wire.TxWitness{signature[2:]} 52 | } else if lenSignature > (2+64+34) && lenSignature <= (2+72+34) { 53 | wit = wire.TxWitness{signature[2 : lenSignature-34], signature[lenSignature-33 : lenSignature]} 54 | } else { 55 | fmt.Println("b64 sig:", hex.EncodeToString(signature)) 56 | fmt.Println("pkScript:", hex.EncodeToString([]byte(data.PkScript))) 57 | fmt.Println("b64 sig length invalid") 58 | return id, false 59 | } 60 | 61 | // check sig 62 | if ok := bip322.VerifySignature(wit, []byte(data.PkScript), message); !ok { 63 | log.Printf("CheckFunctionSigVerify. content: %s", content) 64 | fmt.Println("sig invalid") 65 | return id, false 66 | } 67 | return id, true 68 | } 69 | 70 | // CheckAmountVerify Verify the legality of the brc20 tick amt. 71 | func CheckAmountVerify(amtStr string, nDecimal uint8) (amt *decimal.Decimal, ok bool) { 72 | // check amount 73 | amt, err := decimal.NewDecimalFromString(amtStr, int(nDecimal)) 74 | if err != nil { 75 | return nil, false 76 | } 77 | if amt.Sign() < 0 { 78 | return nil, false 79 | } 80 | 81 | return amt, true 82 | } 83 | 84 | // CheckTickVerify Verify the legality of the brc20 tick amt. 85 | func (g *BRC20ModuleIndexer) CheckTickVerify(tick string, amtStr string) (amt *decimal.Decimal, ok bool) { 86 | uniqueLowerTicker := strings.ToLower(tick) 87 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 88 | if !ok { 89 | return 90 | } 91 | 92 | if amtStr == "" { 93 | return nil, true 94 | } 95 | 96 | tinfo := tokenInfo.Deploy 97 | 98 | // check amount 99 | amt, err := decimal.NewDecimalFromString(amtStr, int(tinfo.Decimal)) 100 | if err != nil { 101 | return nil, false 102 | } 103 | if amt.Sign() < 0 || amt.Cmp(tinfo.Max) > 0 { 104 | return nil, false 105 | } 106 | 107 | return amt, true 108 | } 109 | 110 | // CheckTickVerify Verify the legality of the brc20 tick amt. 111 | func (g *BRC20ModuleIndexer) CheckTickVerifyBigInt(tick string, amtStr string) (amt *decimal.Decimal, ok bool) { 112 | uniqueLowerTicker := strings.ToLower(tick) 113 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 114 | if !ok { 115 | return 116 | } 117 | 118 | if amtStr == "" { 119 | return nil, true 120 | } 121 | 122 | tinfo := tokenInfo.Deploy 123 | 124 | // check amount 125 | amt, err := decimal.NewDecimalFromString(amtStr, 0) 126 | if err != nil { 127 | return nil, false 128 | } 129 | amt.Precition = uint(tinfo.Decimal) 130 | if amt.Sign() < 0 || amt.Cmp(tinfo.Max) > 0 { 131 | return nil, false 132 | } 133 | 134 | return amt, true 135 | } 136 | 137 | func GetLowerInnerPairNameByToken(token0, token1 string) (poolPair string) { 138 | token0 = strings.ToLower(token0) 139 | token1 = strings.ToLower(token1) 140 | 141 | if token0 > token1 { 142 | poolPair = fmt.Sprintf("%s%s%s", string([]byte{uint8(len(token1))}), token1, token0) 143 | } else { 144 | poolPair = fmt.Sprintf("%s%s%s", string([]byte{uint8(len(token0))}), token0, token1) 145 | } 146 | return poolPair 147 | } 148 | 149 | // GetEachItemLengthOfCommitJsonData Get the actual number of bytes occupied by obj in the data list 150 | func GetEachItemLengthOfCommitJsonData(body []byte) (results []uint64, err error) { 151 | decoder := json.NewDecoder(bytes.NewReader(body)) 152 | const ( 153 | TOKEN_TYPE_OBJ = iota 154 | TOKEN_TYPE_ARR 155 | ) 156 | curType := -1 157 | const ( 158 | TOKEN_VALUE_MAPKEY = iota 159 | TOKEN_VALUE_MAPVALUE 160 | TOKEN_VALUE_ARRAY_ELEMENT 161 | ) 162 | curEle := -1 163 | 164 | indentLevel := 0 165 | 166 | stack := list.New() 167 | 168 | setEleType := func() { 169 | switch curType { 170 | case TOKEN_TYPE_OBJ: 171 | curEle = TOKEN_VALUE_MAPKEY 172 | case TOKEN_TYPE_ARR: 173 | curEle = TOKEN_VALUE_ARRAY_ELEMENT 174 | } 175 | } 176 | 177 | readyDataProcess := false 178 | startDataProcess := false 179 | var lastPos uint64 180 | 181 | for { 182 | tok, err := decoder.Token() 183 | // Return the next unprocessed token. 184 | if err == io.EOF { 185 | break 186 | } else if err != nil { 187 | return nil, err 188 | } 189 | 190 | offset := decoder.InputOffset() 191 | 192 | switch tok := tok.(type) { 193 | // Based on the token type, appropriate processing is performed. 194 | case json.Delim: 195 | 196 | switch tok { 197 | case '{': 198 | 199 | if indentLevel == 2 && readyDataProcess && startDataProcess { 200 | // Step 3: Record start offset at '{' character. 201 | lastPos = uint64(offset) 202 | } 203 | 204 | stack.PushBack(TOKEN_TYPE_OBJ) 205 | curType = TOKEN_TYPE_OBJ 206 | setEleType() 207 | indentLevel += 1 208 | case '}': 209 | 210 | if indentLevel == 3 && readyDataProcess && startDataProcess { 211 | // Step 4: Record length at '}' character. 212 | results = append(results, uint64(offset)-lastPos+1) 213 | } 214 | 215 | stack.Remove(stack.Back()) 216 | if stack.Len() > 0 { 217 | curType = stack.Back().Value.(int) 218 | setEleType() 219 | } 220 | indentLevel -= 1 221 | case '[': 222 | 223 | if indentLevel == 1 && readyDataProcess && !startDataProcess { 224 | // Step 2: Start formally counting after '['. 225 | results = nil 226 | startDataProcess = true 227 | } 228 | 229 | stack.PushBack(TOKEN_TYPE_ARR) 230 | curType = TOKEN_TYPE_ARR 231 | setEleType() 232 | indentLevel += 1 233 | case ']': 234 | 235 | if indentLevel == 2 && readyDataProcess && startDataProcess { 236 | // Step 5: End the statistics after ']'. 237 | readyDataProcess = false 238 | startDataProcess = false 239 | } 240 | 241 | stack.Remove(stack.Back()) 242 | if stack.Len() > 0 { 243 | curType = stack.Back().Value.(int) 244 | setEleType() 245 | } 246 | indentLevel -= 1 247 | } 248 | 249 | default: 250 | switch curType { 251 | case TOKEN_TYPE_OBJ: 252 | switch curEle { 253 | case TOKEN_VALUE_MAPKEY: 254 | 255 | if indentLevel == 1 { 256 | if tok == "data" { 257 | // Step 1: Mark the data start, and initialize the marker and result variables. 258 | results = nil 259 | readyDataProcess = true 260 | startDataProcess = false 261 | } else { 262 | // Step 6: Mark complete. 263 | readyDataProcess = false 264 | startDataProcess = false 265 | } 266 | } 267 | 268 | curEle = TOKEN_VALUE_MAPVALUE 269 | case TOKEN_VALUE_MAPVALUE: 270 | curEle = TOKEN_VALUE_MAPKEY 271 | } 272 | } 273 | } 274 | } 275 | 276 | return results, nil 277 | } 278 | -------------------------------------------------------------------------------- /indexer/module_cond_approve.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/unisat-wallet/libbrc20-indexer/conf" 12 | "github.com/unisat-wallet/libbrc20-indexer/constant" 13 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 14 | "github.com/unisat-wallet/libbrc20-indexer/model" 15 | "github.com/unisat-wallet/libbrc20-indexer/utils" 16 | ) 17 | 18 | func (g *BRC20ModuleIndexer) GetConditionalApproveInfoByKey(createIdxKey string) ( 19 | approveInfo *model.InscriptionBRC20SwapConditionalApproveInfo, isInvalid bool) { 20 | var ok bool 21 | // approve 22 | approveInfo, ok = g.InscriptionsValidConditionalApproveMap[createIdxKey] 23 | if !ok { 24 | approveInfo, ok = g.InscriptionsInvalidConditionalApproveMap[createIdxKey] 25 | if !ok { 26 | approveInfo = nil 27 | } 28 | isInvalid = true 29 | } 30 | 31 | return approveInfo, isInvalid 32 | } 33 | 34 | func (g *BRC20ModuleIndexer) ProcessConditionalApprove(data *model.InscriptionBRC20Data, approveInfo *model.InscriptionBRC20SwapConditionalApproveInfo, isInvalid bool) error { 35 | inscriptionId := approveInfo.Data.GetInscriptionId() 36 | log.Printf("parse move approve. inscription id: %s", inscriptionId) 37 | 38 | // ticker 39 | uniqueLowerTicker := strings.ToLower(approveInfo.Tick) 40 | if _, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker]; !ok { 41 | return errors.New("approve, invalid ticker") 42 | } 43 | 44 | moduleInfo, ok := g.ModulesInfoMap[approveInfo.Module] 45 | if !ok { 46 | log.Printf("ProcessBRC20ConditionalApprove send approve, but module invalid. txid: %s", 47 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 48 | ) 49 | return errors.New("approve, module invalid") 50 | } 51 | 52 | // global invalid history 53 | if isInvalid { 54 | // global history 55 | history := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_CONDITIONAL_APPROVE, approveInfo.Data, data, nil, !isInvalid) 56 | moduleInfo.History = append(moduleInfo.History, history) 57 | return nil 58 | } 59 | 60 | var amt *decimal.Decimal 61 | var events []*model.ConditionalApproveEvent 62 | 63 | // First move 64 | // If sent to self or if the fee is deducted, all balances will be directly refunded to the user's swap balance, and the inscription balance will be used up and become void 65 | // If sent to another party, mark current receiver as an agent; if the address cannot be unlocked, it will cause the balance to be stuck, money loss needs to be borne by the user. 66 | // Multiple moves 67 | // If the sending address and the receiving address are different, or if the fee is deducted, all balances will be directly refunded to the user's swap balance, and the inscription balance will be used up and become void 68 | // If the sending address and the receiving address are the same, carry out transfer scanning 69 | // 70 | // In the same transaction, it's possible to deposit repeatedly into different instances of the same type of module, or deposit into different modules 71 | // Although this causes the issue of repeated deposits, it can maintain complete independence between module instances. 72 | 73 | senderPkScript := string(approveInfo.Data.PkScript) 74 | receiverPkScript := string(data.PkScript) 75 | if !approveInfo.HasMoved { 76 | approveInfo.HasMoved = true 77 | if data.Satoshi == 0 || senderPkScript == receiverPkScript { 78 | receiverPkScript = senderPkScript 79 | amt = approveInfo.Amount 80 | approveInfo.Balance = nil 81 | 82 | log.Printf("generate new approve event return self with out move, id: %s", inscriptionId) 83 | log.Printf("generate new approve event amt: %s", amt.String()) 84 | // Returned directly from the start 85 | event := model.NewConditionalApproveEvent(senderPkScript, receiverPkScript, amt, approveInfo.Balance, data, approveInfo, "", "") 86 | events = append(events, event) 87 | 88 | } else if senderPkScript != receiverPkScript { 89 | approveInfo.DelegatorPkScript = receiverPkScript 90 | return nil 91 | } // no else 92 | } else { 93 | senderPkScript = approveInfo.DelegatorPkScript 94 | if data.Satoshi == 0 || senderPkScript != receiverPkScript { 95 | senderPkScript = approveInfo.OwnerPkScript 96 | receiverPkScript = senderPkScript 97 | amt = approveInfo.Balance 98 | approveInfo.Balance = nil 99 | 100 | log.Printf("generate new approve event return self after move, id: %s", inscriptionId) 101 | log.Printf("generate new approve event amt: %s", amt.String()) 102 | // Subsequent direct return 103 | event := model.NewConditionalApproveEvent(senderPkScript, receiverPkScript, amt, approveInfo.Balance, data, approveInfo, "", "") 104 | events = append(events, event) 105 | 106 | } else if senderPkScript == receiverPkScript { 107 | if g.ThisTxId != data.TxId { 108 | g.TxStaticTransferStatesForConditionalApprove = nil 109 | g.ThisTxId = data.TxId 110 | } 111 | events = g.GenerateApproveEventsByApprove(approveInfo.OwnerPkScript, approveInfo.Balance, 112 | data, approveInfo) 113 | } // no else 114 | } 115 | 116 | return g.ProcessConditionalApproveEvents(events) 117 | } 118 | 119 | func (g *BRC20ModuleIndexer) ProcessConditionalApproveEvents(events []*model.ConditionalApproveEvent) error { 120 | // produce conditional approve events 121 | for _, event := range events { 122 | inscriptionId := event.FromData.GetInscriptionId() 123 | 124 | // from address 125 | addressFrom, err := utils.GetAddressFromScript([]byte(event.From), conf.GlobalNetParams) 126 | if err != nil { 127 | addressFrom = hex.EncodeToString([]byte(event.From)) 128 | } 129 | // to address 130 | addressTo, err := utils.GetAddressFromScript([]byte(event.To), conf.GlobalNetParams) 131 | if err != nil { 132 | addressTo = hex.EncodeToString([]byte(event.From)) 133 | } 134 | log.Printf("process approve event. inscription id: %s, from: %s, to: %s, amt: %s, balance: %s", 135 | inscriptionId, 136 | addressFrom, 137 | addressTo, 138 | event.Amount.String(), 139 | event.Balance.String()) 140 | 141 | // ticker 142 | uniqueLowerTicker := strings.ToLower(event.Tick) 143 | if _, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker]; !ok { 144 | return errors.New("approve event, invalid ticker") 145 | } 146 | 147 | moduleInfo, ok := g.ModulesInfoMap[event.Module] 148 | if !ok { 149 | return errors.New("approve event, module invalid") 150 | } 151 | 152 | // global history 153 | data := &model.BRC20SwapHistoryCondApproveData{ 154 | Tick: event.Tick, 155 | Amount: event.Amount.String(), 156 | Balance: event.Balance.String(), 157 | TransferInscriptionId: event.TransferInscriptionId, 158 | TransferMax: event.TransferMax, 159 | } 160 | history := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_CONDITIONAL_APPROVE, &event.FromData, &event.ToData, data, true) 161 | moduleInfo.History = append(moduleInfo.History, history) 162 | 163 | // from 164 | // get user's tokens to update 165 | fromUserTokens, ok := moduleInfo.UsersTokenBalanceDataMap[string(event.From)] 166 | if !ok { 167 | log.Printf("ProcessBRC20ConditionalApprove send from user missing. height: %d, txidx: %d", 168 | event.ToData.Height, 169 | event.ToData.TxIdx, 170 | ) 171 | return errors.New("approve, send from user missing") 172 | } 173 | // get tokenBalance to update 174 | fromTokenBalance, ok := fromUserTokens[uniqueLowerTicker] 175 | if !ok { 176 | log.Printf("ProcessBRC20ConditionalApprove send from ticker missing. height: %d, txidx: %d", 177 | event.ToData.Height, 178 | event.ToData.TxIdx, 179 | ) 180 | return errors.New("approve, send from ticker missing") 181 | } 182 | 183 | // Cross-check whether the approve inscription exists. 184 | if _, ok := fromTokenBalance.ValidConditionalApproveMap[event.ToData.CreateIdxKey]; !ok { 185 | log.Printf("ProcessBRC20ConditionalApprove send from approve missing(dup approve?). height: %d, txidx: %d", 186 | event.ToData.Height, 187 | event.ToData.TxIdx, 188 | ) 189 | return errors.New("approve, send from approve missing(dup)") 190 | } 191 | 192 | // to 193 | tokenBalance := moduleInfo.GetUserTokenBalance(event.Tick, event.To) 194 | 195 | // set from 196 | fromTokenBalance.CondApproveableBalance = fromTokenBalance.CondApproveableBalance.Sub(event.Amount) 197 | // delete(fromTokenBalance.ValidConditionalApproveMap, data.CreateIdxKey) 198 | 199 | fromTokenBalance.UpdateHeight = g.BestHeight 200 | 201 | // fixme: history.Data 202 | fromHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_APPROVE_FROM, &event.FromData, &event.ToData, nil, true) 203 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 204 | 205 | // set to 206 | if event.ToData.BlockTime > 0 { 207 | tokenBalance.SwapAccountBalanceSafe = tokenBalance.SwapAccountBalanceSafe.Add(event.Amount) 208 | } 209 | tokenBalance.SwapAccountBalance = tokenBalance.SwapAccountBalance.Add(event.Amount) 210 | 211 | tokenBalance.UpdateHeight = g.BestHeight 212 | 213 | // fixme: history.Data 214 | toHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_SWAP_TYPE_N_APPROVE_TO, &event.FromData, &event.ToData, nil, true) 215 | tokenBalance.History = append(tokenBalance.History, toHistory) 216 | 217 | // record state 218 | stateBalance := moduleInfo.GetTickConditionalApproveStateBalance(event.Tick) 219 | if event.From == event.To { 220 | stateBalance.BalanceCancelApprove = stateBalance.BalanceCancelApprove.Add(event.Amount) 221 | } else { 222 | stateBalance.BalanceApprove = stateBalance.BalanceApprove.Add(event.Amount) 223 | } 224 | } 225 | 226 | for _, event := range events { 227 | event.ApproveInfo.UpdateHeight = g.BestHeight 228 | event.ApproveInfo.Balance = event.Balance 229 | } 230 | return nil 231 | } 232 | 233 | func (g *BRC20ModuleIndexer) ProcessInscribeConditionalApprove(data *model.InscriptionBRC20Data) error { 234 | if data.Height >= conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 235 | return errors.New("invalid operation") 236 | } 237 | 238 | var body model.InscriptionBRC20ModuleSwapApproveContent 239 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 240 | log.Printf("parse approve json failed. txid: %s", 241 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 242 | ) 243 | return err 244 | } 245 | 246 | // lower case only 247 | if body.Module != strings.ToLower(body.Module) { 248 | return errors.New("module id invalid") 249 | } 250 | 251 | moduleInfo, ok := g.ModulesInfoMap[body.Module] 252 | if !ok { // invalid module 253 | return errors.New("module invalid") 254 | } 255 | 256 | if len(body.Tick) != 4 { 257 | return errors.New("tick invalid") 258 | } 259 | 260 | uniqueLowerTicker := strings.ToLower(body.Tick) 261 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 262 | if !ok { 263 | return errors.New("tick not exist") 264 | } 265 | tinfo := tokenInfo.Deploy 266 | 267 | // check amount 268 | amt, err := decimal.NewDecimalFromString(body.Amount, int(tinfo.Decimal)) 269 | if err != nil { 270 | return errors.New(fmt.Sprintf("cond approve amount invalid: %s", body.Amount)) 271 | } 272 | if amt.Sign() <= 0 || amt.Cmp(tinfo.Max) > 0 { 273 | return errors.New("amount out of range") 274 | } 275 | 276 | balanceCondApprove := decimal.NewDecimalCopy(amt) 277 | 278 | body.Tick = tokenInfo.Ticker 279 | condApproveInfo := &model.InscriptionBRC20SwapConditionalApproveInfo{ 280 | Data: data, 281 | } 282 | condApproveInfo.UpdateHeight = g.BestHeight 283 | 284 | condApproveInfo.Module = body.Module 285 | condApproveInfo.Tick = tokenInfo.Ticker 286 | condApproveInfo.Amount = balanceCondApprove 287 | condApproveInfo.Balance = decimal.NewDecimalCopy(balanceCondApprove) 288 | condApproveInfo.OwnerPkScript = data.PkScript 289 | 290 | // global history 291 | historyData := &model.BRC20SwapHistoryCondApproveData{ 292 | Tick: condApproveInfo.Tick, 293 | Amount: condApproveInfo.Amount.String(), 294 | Balance: condApproveInfo.Balance.String(), 295 | } 296 | history := model.NewBRC20ModuleHistory(false, constant.BRC20_HISTORY_SWAP_TYPE_N_INSCRIBE_CONDITIONAL_APPROVE, data, data, historyData, true) 297 | moduleInfo.History = append(moduleInfo.History, history) 298 | 299 | moduleTokenBalance := moduleInfo.GetUserTokenBalance(condApproveInfo.Tick, data.PkScript) 300 | if moduleTokenBalance.AvailableBalance.Cmp(balanceCondApprove) < 0 { // invalid 301 | history.Valid = false 302 | g.InscriptionsInvalidConditionalApproveMap[data.CreateIdxKey] = condApproveInfo 303 | } else { 304 | history.Valid = true 305 | // The available balance here will be directly deducted and transferred to ApproveableBalance. 306 | moduleTokenBalance.AvailableBalanceSafe = moduleTokenBalance.AvailableBalanceSafe.Sub(balanceCondApprove) 307 | moduleTokenBalance.AvailableBalance = moduleTokenBalance.AvailableBalance.Sub(balanceCondApprove) 308 | 309 | moduleTokenBalance.CondApproveableBalance = moduleTokenBalance.CondApproveableBalance.Add(balanceCondApprove) 310 | 311 | // Update personal approve lookup table ValidApproveMap 312 | if moduleTokenBalance.ValidConditionalApproveMap == nil { 313 | moduleTokenBalance.ValidConditionalApproveMap = make(map[string]*model.InscriptionBRC20Data, 1) 314 | } 315 | moduleTokenBalance.ValidConditionalApproveMap[data.CreateIdxKey] = data 316 | 317 | moduleTokenBalance.UpdateHeight = g.BestHeight 318 | 319 | // Update global approve lookup table 320 | g.InscriptionsValidConditionalApproveMap[data.CreateIdxKey] = condApproveInfo 321 | // g.InscriptionsValidBRC20DataMap[data.CreateIdxKey] = condApproveInfo.Data // fixme 322 | 323 | // record state 324 | stateBalance := moduleInfo.GetTickConditionalApproveStateBalance(condApproveInfo.Tick) 325 | stateBalance.BalanceNewApprove = stateBalance.BalanceNewApprove.Add(balanceCondApprove) 326 | } 327 | 328 | return nil 329 | } 330 | -------------------------------------------------------------------------------- /indexer/module_create.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/unisat-wallet/libbrc20-indexer/conf" 11 | "github.com/unisat-wallet/libbrc20-indexer/constant" 12 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 13 | "github.com/unisat-wallet/libbrc20-indexer/model" 14 | "github.com/unisat-wallet/libbrc20-indexer/utils" 15 | ) 16 | 17 | func (g *BRC20ModuleIndexer) ProcessCreateModule(data *model.InscriptionBRC20Data) error { 18 | var body model.InscriptionBRC20ModuleDeploySwapContent 19 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 20 | log.Printf("parse create module json failed. txid: %s", 21 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 22 | ) 23 | return err 24 | } 25 | 26 | if conf.MODULE_SWAP_SOURCE_INSCRIPTION_ID != body.Source { 27 | return errors.New(fmt.Sprintf("source not match: %s", body.Source)) 28 | } 29 | 30 | inscriptionId := data.GetInscriptionId() 31 | log.Printf("create module: %s", inscriptionId) 32 | 33 | if _, ok := g.ModulesInfoMap[inscriptionId]; ok { 34 | return errors.New("dup module deploy") // impossible 35 | } 36 | 37 | // feeRateSwap 38 | feeRateSwap, ok := body.Init["swap_fee_rate"] 39 | if !ok { 40 | feeRateSwap = "0" 41 | } 42 | if _, ok := CheckAmountVerify(feeRateSwap, 3); !ok { 43 | return errors.New("swap fee invalid") 44 | } 45 | 46 | // gasTick 47 | gasTick, ok := body.Init["gas_tick"] 48 | if !ok { 49 | // gas is not optional 50 | return errors.New("gas_tick missing") 51 | } 52 | if _, ok := g.CheckTickVerify(gasTick, ""); !ok { 53 | log.Printf("create module gas tick[%s] invalid", gasTick) 54 | return errors.New("gas_tick invalid") 55 | } 56 | 57 | // sequencer default 58 | sequencerPkScript := data.PkScript 59 | if sequencer, ok := body.Init["sequencer"]; ok { 60 | if pk, err := utils.GetPkScriptByAddress(sequencer, conf.GlobalNetParams); err != nil { 61 | return errors.New("sequencer invalid") 62 | } else { 63 | sequencerPkScript = string(pk) 64 | } 65 | } else { 66 | return errors.New("sequencer missing") 67 | } 68 | 69 | // gasTo default 70 | gasToPkScript := data.PkScript 71 | if gasTo, ok := body.Init["gas_to"]; ok { 72 | if pk, err := utils.GetPkScriptByAddress(gasTo, conf.GlobalNetParams); err != nil { 73 | return errors.New("gasTo invalid") 74 | } else { 75 | gasToPkScript = string(pk) 76 | } 77 | } else { 78 | return errors.New("gas_to missing") 79 | } 80 | 81 | // lpFeeTo default 82 | lpFeeToPkScript := data.PkScript 83 | if lpFeeTo, ok := body.Init["fee_to"]; ok { 84 | if pk, err := utils.GetPkScriptByAddress(lpFeeTo, conf.GlobalNetParams); err != nil { 85 | return errors.New("lpFeeTo invalid") 86 | } else { 87 | lpFeeToPkScript = string(pk) 88 | } 89 | } else { 90 | return errors.New("fee_to missing") 91 | } 92 | 93 | m := &model.BRC20ModuleSwapInfo{ 94 | ID: inscriptionId, 95 | Name: body.Name, 96 | DeployerPkScript: data.PkScript, // deployer 97 | SequencerPkScript: sequencerPkScript, // Sequencer 98 | GasToPkScript: gasToPkScript, 99 | LpFeePkScript: lpFeeToPkScript, 100 | 101 | FeeRateSwap: feeRateSwap, 102 | GasTick: gasTick, 103 | 104 | History: make([]*model.BRC20ModuleHistory, 0), 105 | 106 | // runtime for commit 107 | CommitInvalidMap: make(map[string]struct{}, 0), 108 | CommitIdChainMap: make(map[string]struct{}, 0), 109 | CommitIdMap: make(map[string]struct{}, 0), 110 | 111 | // runtime for holders 112 | // token holders in module 113 | // ticker of users in module [address][tick]balanceData 114 | UsersTokenBalanceDataMap: make(map[string]map[string]*model.BRC20ModuleTokenBalance, 0), 115 | 116 | // token balance of address in module [tick][address]balanceData 117 | TokenUsersBalanceDataMap: make(map[string]map[string]*model.BRC20ModuleTokenBalance, 0), 118 | 119 | // swap 120 | // lp token balance of address in module [pool][address]balance 121 | LPTokenUsersBalanceMap: make(map[string]map[string]*decimal.Decimal, 0), 122 | LPTokenUsersBalanceUpdatedMap: make(map[string]struct{}, 0), 123 | 124 | // lp token of users in module [moduleid][address][pool]balance 125 | UsersLPTokenBalanceMap: make(map[string]map[string]*decimal.Decimal, 0), 126 | 127 | // swap total balance 128 | // total balance of pool in module [pool]balanceData 129 | SwapPoolTotalBalanceDataMap: make(map[string]*model.BRC20ModulePoolTotalBalance, 0), 130 | 131 | ConditionalApproveStateBalanceDataMap: make(map[string]*model.BRC20ModuleConditionalApproveStateBalance, 0), 132 | } 133 | 134 | m.UpdateHeight = data.Height 135 | 136 | // deployInfo := model.NewInscriptionBRC20SwapInfo(data) 137 | // deployInfo.Module = inscriptionId 138 | 139 | history := model.NewBRC20ModuleHistory(false, constant.BRC20_HISTORY_MODULE_TYPE_N_INSCRIBE_MODULE, data, data, nil, true) 140 | m.History = append(m.History, history) 141 | 142 | g.ModulesInfoMap[inscriptionId] = m 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /indexer/module_withdraw.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/unisat-wallet/libbrc20-indexer/conf" 12 | "github.com/unisat-wallet/libbrc20-indexer/constant" 13 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 14 | "github.com/unisat-wallet/libbrc20-indexer/model" 15 | "github.com/unisat-wallet/libbrc20-indexer/utils" 16 | ) 17 | 18 | func (g *BRC20ModuleIndexer) GetWithdrawInfoByKey(createIdxKey string) ( 19 | withdrawInfo *model.InscriptionBRC20SwapInfo) { 20 | var ok bool 21 | // withdraw 22 | withdrawInfo, ok = g.InscriptionsWithdrawMap[createIdxKey] 23 | if !ok { 24 | withdrawInfo = nil 25 | } 26 | 27 | return withdrawInfo 28 | } 29 | 30 | func (g *BRC20ModuleIndexer) ProcessWithdraw(data *model.InscriptionBRC20Data, withdrawInfo *model.InscriptionBRC20SwapInfo) error { 31 | // ticker 32 | uniqueLowerTicker := strings.ToLower(withdrawInfo.Tick) 33 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 34 | if !ok { 35 | log.Printf("ProcessWithdraw send withdraw, but ticker invalid. txid: %s", 36 | utils.HashString([]byte(data.TxId)), 37 | ) 38 | return errors.New("transfer, invalid ticker") 39 | } 40 | 41 | moduleInfo, ok := g.ModulesInfoMap[withdrawInfo.Module] 42 | if !ok { 43 | log.Printf("ProcessBRC20Withdraw send withdraw, but ticker invalid. txid: %s", 44 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 45 | ) 46 | return errors.New("withdraw, module invalid") 47 | } 48 | 49 | var isInvalid bool 50 | 51 | // global history fixme 52 | // if g.EnableHistory { 53 | // historyObj := model.NewBRC20History(constant.BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW, !isInvalid, true, withdrawInfo, nil, data) 54 | // history := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 55 | 56 | // tokenInfo.History = append(tokenInfo.History, history) 57 | // // tokenInfo.HistoryWithdraw = append(tokenInfo.HistoryTransfer, history) 58 | // if !isInvalid { 59 | // // all history 60 | // g.AllHistory = append(g.AllHistory, history) 61 | // } 62 | // } 63 | 64 | // from 65 | // get user's tokens to update 66 | fromUserTokens, ok := moduleInfo.UsersTokenBalanceDataMap[string(withdrawInfo.Data.PkScript)] 67 | if !ok { 68 | log.Printf("ProcessBRC20Withdraw send from user missing. height: %d, txidx: %d", 69 | data.Height, 70 | data.TxIdx, 71 | ) 72 | return errors.New("withdraw, send from user missing") 73 | } 74 | // get tokenBalance to update 75 | fromTokenBalance, ok := fromUserTokens[uniqueLowerTicker] 76 | if !ok { 77 | log.Printf("ProcessBRC20Withdraw send from ticker missing. height: %d, txidx: %d", 78 | data.Height, 79 | data.TxIdx, 80 | ) 81 | return errors.New("withdraw, send from ticker missing") 82 | } 83 | 84 | // Cross-check whether the withdraw-inscription exists. 85 | if _, ok := fromTokenBalance.ReadyToWithdrawMap[data.CreateIdxKey]; !ok { 86 | log.Printf("ProcessBRC20Withdraw send from withdraw missing(dup withdraw?). height: %d, txidx: %d", 87 | data.Height, 88 | data.TxIdx, 89 | ) 90 | return errors.New("withdraw, send from withdraw missing(dup)") 91 | } 92 | 93 | // available > amt 94 | balanceWithdraw := withdrawInfo.Amount 95 | fromTokenBalance.ReadyToWithdrawAmount = fromTokenBalance.ReadyToWithdrawAmount.Sub(balanceWithdraw) 96 | delete(fromTokenBalance.ReadyToWithdrawMap, data.CreateIdxKey) 97 | fromTokenBalance.UpdateHeight = data.Height 98 | 99 | if fromTokenBalance.AvailableBalance.Cmp(balanceWithdraw) < 0 { // invalid 100 | isInvalid = true 101 | } 102 | 103 | // to address 104 | receiverPkScript := string(data.PkScript) 105 | if data.Satoshi == 0 { 106 | receiverPkScript = string(withdrawInfo.Data.PkScript) 107 | data.PkScript = receiverPkScript 108 | } 109 | 110 | // global history 111 | historyData := &model.BRC20SwapHistoryWithdrawData{ 112 | Tick: withdrawInfo.Tick, 113 | Amount: withdrawInfo.Amount.String(), 114 | } 115 | history := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW, withdrawInfo.Data, data, historyData, !isInvalid) 116 | moduleInfo.History = append(moduleInfo.History, history) 117 | if isInvalid { 118 | // from invalid history 119 | fromHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_FROM, withdrawInfo.Data, data, nil, false) 120 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 121 | return nil 122 | } 123 | 124 | // set from 125 | // The available balance here needs to be directly deducted and transferred to WithdrawableBalance. 126 | fromTokenBalance.AvailableBalanceSafe = fromTokenBalance.AvailableBalanceSafe.Sub(balanceWithdraw) 127 | fromTokenBalance.AvailableBalance = fromTokenBalance.AvailableBalance.Sub(balanceWithdraw) 128 | 129 | fromHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_FROM, withdrawInfo.Data, data, nil, true) 130 | fromTokenBalance.History = append(fromTokenBalance.History, fromHistory) 131 | 132 | // to 133 | tokenBalance := g.GetUserTokenBalance(withdrawInfo.Tick, receiverPkScript) 134 | // set to 135 | tokenBalance.UpdateHeight = data.Height 136 | 137 | if data.BlockTime > 0 { 138 | tokenBalance.AvailableBalanceSafe = tokenBalance.AvailableBalanceSafe.Add(withdrawInfo.Amount) 139 | } 140 | tokenBalance.AvailableBalance = tokenBalance.AvailableBalance.Add(withdrawInfo.Amount) 141 | 142 | // burn 143 | if len(receiverPkScript) == 1 && []byte(receiverPkScript)[0] == 0x6a { 144 | tokenInfo.Deploy.Burned = tokenInfo.Deploy.Burned.Add(withdrawInfo.Amount) 145 | } 146 | 147 | // fixme: add user module history 148 | // if g.EnableHistory { 149 | // historyObj := model.NewBRC20History(constant.BRC20_HISTORY_TYPE_N_RECEIVE, true, true, withdrawInfo, tokenBalance, data) 150 | // toHistory := g.UpdateHistoryHeightAndGetHistoryIndex(historyObj) 151 | 152 | // tokenBalance.History = append(tokenBalance.History, toHistory) 153 | // tokenBalance.HistoryReceive = append(tokenBalance.HistoryReceive, toHistory) 154 | 155 | // userHistoryTo := g.GetBRC20HistoryByUser(receiverPkScript) 156 | // userHistoryTo.History = append(userHistoryTo.History, toHistory) 157 | // } 158 | 159 | // toHistory := model.NewBRC20ModuleHistory(true, constant.BRC20_HISTORY_MODULE_TYPE_N_WITHDRAW_TO, withdrawInfo.Data, data, nil, true) 160 | // tokenBalance.History = append(tokenBalance.History, toHistory) 161 | 162 | //////////////////////////////////////////////////////////////// 163 | // withdraw to a module, is NOT deposit 164 | return nil 165 | } 166 | 167 | func (g *BRC20ModuleIndexer) ProcessInscribeWithdraw(data *model.InscriptionBRC20Data) error { 168 | var body model.InscriptionBRC20ModuleWithdrawContent 169 | if err := json.Unmarshal(data.ContentBody, &body); err != nil { 170 | log.Printf("parse module withdraw json failed. txid: %s", 171 | hex.EncodeToString(utils.ReverseBytes([]byte(data.TxId))), 172 | ) 173 | return err 174 | } 175 | 176 | // lower case only 177 | if body.Module != strings.ToLower(body.Module) { 178 | return errors.New("module id invalid") 179 | } 180 | 181 | moduleInfo, ok := g.ModulesInfoMap[body.Module] 182 | if !ok { // invalid module 183 | return errors.New("module invalid") 184 | } 185 | 186 | if data.Height < conf.ENABLE_SWAP_WITHDRAW_HEIGHT { 187 | return errors.New("module withdraw disable") 188 | } 189 | 190 | if len(body.Tick) != 4 && len(body.Tick) != 5 { 191 | return errors.New("tick invalid") 192 | } 193 | uniqueLowerTicker := strings.ToLower(body.Tick) 194 | tokenInfo, ok := g.InscriptionsTickerInfoMap[uniqueLowerTicker] 195 | if !ok { 196 | return errors.New("tick not exist") 197 | } 198 | tinfo := tokenInfo.Deploy 199 | 200 | amt, err := decimal.NewDecimalFromString(body.Amount, int(tinfo.Decimal)) 201 | if err != nil { 202 | return errors.New(fmt.Sprintf("withdraw amount invalid: %s", body.Amount)) 203 | } 204 | if amt.Sign() <= 0 || amt.Cmp(tinfo.Max) > 0 { 205 | return errors.New("amount out of range") 206 | } 207 | 208 | balanceWithdraw := decimal.NewDecimalCopy(amt) 209 | 210 | // Unify ticker case 211 | body.Tick = tokenInfo.Ticker 212 | // Set up withdraw data for subsequent use. 213 | withdrawInfo := &model.InscriptionBRC20SwapInfo{ 214 | Data: data, 215 | } 216 | withdrawInfo.Module = body.Module 217 | withdrawInfo.Tick = tokenInfo.Ticker 218 | withdrawInfo.Amount = balanceWithdraw 219 | 220 | // global history 221 | historyData := &model.BRC20SwapHistoryWithdrawData{ 222 | Tick: withdrawInfo.Tick, 223 | Amount: withdrawInfo.Amount.String(), 224 | } 225 | history := model.NewBRC20ModuleHistory(false, constant.BRC20_HISTORY_MODULE_TYPE_N_INSCRIBE_WITHDRAW, data, data, historyData, true) 226 | moduleInfo.History = append(moduleInfo.History, history) 227 | 228 | // Check if the module balance is sufficient to withdraw 229 | moduleTokenBalance := moduleInfo.GetUserTokenBalance(withdrawInfo.Tick, data.PkScript) 230 | { 231 | 232 | moduleTokenBalance.ReadyToWithdrawAmount = moduleTokenBalance.ReadyToWithdrawAmount.Add(balanceWithdraw) 233 | 234 | history.Valid = true 235 | // Update personal withdraw lookup table ReadyToWithdrawMap 236 | if moduleTokenBalance.ReadyToWithdrawMap == nil { 237 | moduleTokenBalance.ReadyToWithdrawMap = make(map[string]*model.InscriptionBRC20Data, 1) 238 | } 239 | moduleTokenBalance.ReadyToWithdrawMap[data.CreateIdxKey] = data 240 | 241 | moduleTokenBalance.UpdateHeight = data.Height 242 | // Update global withdraw lookup table 243 | g.InscriptionsWithdrawMap[data.CreateIdxKey] = withdrawInfo 244 | // g.InscriptionsValidBRC20DataMap[data.CreateIdxKey] = withdrawInfo.Data // fixme 245 | } 246 | 247 | return nil 248 | 249 | } 250 | -------------------------------------------------------------------------------- /indexer/store_into_data.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/gob" 5 | "log" 6 | "os" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 9 | "github.com/unisat-wallet/libbrc20-indexer/model" 10 | ) 11 | 12 | type BRC20ModuleIndexerStore struct { 13 | BestHeight uint32 14 | EnableHistory bool 15 | 16 | HistoryCount uint32 17 | 18 | FirstHistoryByHeight map[uint32]uint32 19 | LastHistoryHeight uint32 20 | 21 | // brc20 base 22 | AllHistory []uint32 23 | UserAllHistory map[string]*model.BRC20UserHistory 24 | 25 | InscriptionsTickerInfoMap map[string]*model.BRC20TokenInfo 26 | UserTokensBalanceData map[string]map[string]*model.BRC20TokenBalance 27 | 28 | InscriptionsValidBRC20DataMap map[string]*model.InscriptionBRC20InfoResp 29 | 30 | // inner valid transfer 31 | InscriptionsValidTransferMap map[string]*model.InscriptionBRC20TickInfo 32 | // inner invalid transfer 33 | InscriptionsInvalidTransferMap map[string]*model.InscriptionBRC20TickInfo 34 | 35 | // module 36 | // all modules info 37 | ModulesInfoMap map[string]*model.BRC20ModuleSwapInfoStore 38 | 39 | // module of users [address]moduleid 40 | UsersModuleWithTokenMap map[string]string 41 | 42 | // module lp of users [address]moduleid 43 | UsersModuleWithLpTokenMap map[string]string 44 | 45 | // runtime for approve 46 | InscriptionsValidApproveMap map[string]*model.InscriptionBRC20SwapInfo // inner valid approve 47 | InscriptionsInvalidApproveMap map[string]*model.InscriptionBRC20SwapInfo 48 | 49 | // runtime for conditional approve 50 | InscriptionsValidConditionalApproveMap map[string]*model.InscriptionBRC20SwapConditionalApproveInfo 51 | InscriptionsInvalidConditionalApproveMap map[string]*model.InscriptionBRC20SwapConditionalApproveInfo 52 | 53 | // runtime for commit 54 | InscriptionsValidCommitMap map[string]*model.InscriptionBRC20Data // inner valid commit by key 55 | InscriptionsInvalidCommitMap map[string]*model.InscriptionBRC20Data 56 | } 57 | 58 | func (g *BRC20ModuleIndexer) Load(fname string) { 59 | log.Printf("loading brc20 ...") 60 | gobFile, err := os.Open(fname) 61 | if err != nil { 62 | log.Printf("open brc20 file failed: %s", err) 63 | return 64 | } 65 | 66 | gob.Register(model.BRC20SwapHistoryApproveData{}) 67 | gob.Register(model.BRC20SwapHistoryCondApproveData{}) 68 | gobDec := gob.NewDecoder(gobFile) 69 | 70 | store := &BRC20ModuleIndexerStore{} 71 | if err := gobDec.Decode(&store); err != nil { 72 | log.Printf("load store failed: %s", err) 73 | return 74 | } 75 | 76 | g.LoadStore(store) 77 | 78 | log.Printf("load brc20 ok") 79 | } 80 | 81 | func (g *BRC20ModuleIndexer) Save(fname string) { 82 | log.Printf("saving brc20 ...") 83 | 84 | gobFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 85 | if err != nil { 86 | log.Printf("open brc20 file failed: %s", err) 87 | return 88 | } 89 | defer gobFile.Close() 90 | 91 | gob.Register(model.BRC20SwapHistoryApproveData{}) 92 | gob.Register(model.BRC20SwapHistoryCondApproveData{}) 93 | 94 | enc := gob.NewEncoder(gobFile) 95 | if err := enc.Encode(g.GetStore()); err != nil { 96 | log.Printf("save store failed: %s", err) 97 | return 98 | } 99 | 100 | log.Printf("save brc20 ok") 101 | } 102 | 103 | func (g *BRC20ModuleIndexer) LoadHistory(fname string) { 104 | log.Printf("loading brc20 history...") 105 | gobFile, err := os.Open(fname) 106 | if err != nil { 107 | log.Printf("open brc20 history file failed: %s", err) 108 | return 109 | } 110 | 111 | gobDec := gob.NewDecoder(gobFile) 112 | 113 | for { 114 | var h []byte 115 | if err := gobDec.Decode(&h); err != nil { 116 | log.Printf("load history data end: %s", err) 117 | break 118 | } 119 | g.HistoryData = append(g.HistoryData, h) 120 | } 121 | log.Printf("load brc20 history ok: %d", len(g.HistoryData)) 122 | } 123 | 124 | func (g *BRC20ModuleIndexer) SaveHistory(fname string) { 125 | log.Printf("saving brc20 history...") 126 | 127 | gobFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 128 | if err != nil { 129 | log.Printf("open brc20 history file failed: %s", err) 130 | return 131 | } 132 | defer gobFile.Close() 133 | 134 | enc := gob.NewEncoder(gobFile) 135 | for _, h := range g.HistoryData { 136 | if err := enc.Encode(h); err != nil { 137 | log.Printf("save history data failed: %s", err) 138 | return 139 | } 140 | } 141 | log.Printf("save brc20 history ok") 142 | } 143 | 144 | func (g *BRC20ModuleIndexer) GetStore() (store *BRC20ModuleIndexerStore) { 145 | store = &BRC20ModuleIndexerStore{ 146 | BestHeight: g.BestHeight, 147 | EnableHistory: g.EnableHistory, 148 | 149 | HistoryCount: g.HistoryCount, 150 | 151 | FirstHistoryByHeight: g.FirstHistoryByHeight, 152 | LastHistoryHeight: g.LastHistoryHeight, 153 | 154 | // brc20 base 155 | AllHistory: g.AllHistory, 156 | UserAllHistory: g.UserAllHistory, 157 | 158 | InscriptionsTickerInfoMap: g.InscriptionsTickerInfoMap, 159 | UserTokensBalanceData: g.UserTokensBalanceData, 160 | 161 | InscriptionsValidBRC20DataMap: g.InscriptionsValidBRC20DataMap, 162 | 163 | // inner valid transfer 164 | InscriptionsValidTransferMap: g.InscriptionsValidTransferMap, 165 | // inner invalid transfer 166 | InscriptionsInvalidTransferMap: g.InscriptionsInvalidTransferMap, 167 | 168 | // module 169 | // all modules info 170 | // module of users [address]moduleid 171 | UsersModuleWithTokenMap: g.UsersModuleWithTokenMap, 172 | 173 | // module lp of users [address]moduleid 174 | UsersModuleWithLpTokenMap: g.UsersModuleWithLpTokenMap, 175 | 176 | // runtime for approve 177 | InscriptionsValidApproveMap: g.InscriptionsValidApproveMap, 178 | InscriptionsInvalidApproveMap: g.InscriptionsInvalidApproveMap, 179 | 180 | // runtime for conditional approve 181 | InscriptionsValidConditionalApproveMap: g.InscriptionsValidConditionalApproveMap, 182 | InscriptionsInvalidConditionalApproveMap: g.InscriptionsInvalidConditionalApproveMap, 183 | 184 | // runtime for commit 185 | InscriptionsValidCommitMap: g.InscriptionsValidCommitMap, 186 | InscriptionsInvalidCommitMap: g.InscriptionsInvalidCommitMap, 187 | } 188 | 189 | store.ModulesInfoMap = make(map[string]*model.BRC20ModuleSwapInfoStore) 190 | for module, info := range g.ModulesInfoMap { 191 | infoStore := &model.BRC20ModuleSwapInfoStore{ 192 | ID: info.ID, 193 | Name: info.Name, 194 | DeployerPkScript: info.DeployerPkScript, 195 | SequencerPkScript: info.SequencerPkScript, 196 | GasToPkScript: info.GasToPkScript, 197 | LpFeePkScript: info.LpFeePkScript, 198 | 199 | FeeRateSwap: info.FeeRateSwap, 200 | GasTick: info.GasTick, 201 | 202 | History: info.History, // fixme 203 | 204 | // runtime for commit 205 | CommitInvalidMap: info.CommitInvalidMap, 206 | CommitIdMap: info.CommitIdMap, 207 | CommitIdChainMap: info.CommitIdChainMap, 208 | 209 | // token holders in module 210 | // ticker of users in module [address][tick]balanceData 211 | UsersTokenBalanceDataMap: info.UsersTokenBalanceDataMap, 212 | 213 | // swap 214 | // lp token balance of address in module [pool][address]balance 215 | LPTokenUsersBalanceMap: info.LPTokenUsersBalanceMap, 216 | 217 | // swap total balance 218 | // total balance of pool in module [pool]balanceData 219 | SwapPoolTotalBalanceDataMap: info.SwapPoolTotalBalanceDataMap, 220 | 221 | // module deposit/withdraw state [tick]balanceData 222 | ConditionalApproveStateBalanceDataMap: info.ConditionalApproveStateBalanceDataMap, 223 | } 224 | 225 | store.ModulesInfoMap[module] = infoStore 226 | } 227 | 228 | return store 229 | 230 | } 231 | 232 | func (g *BRC20ModuleIndexer) LoadStore(store *BRC20ModuleIndexerStore) { 233 | g.BestHeight = store.BestHeight 234 | g.EnableHistory = store.EnableHistory 235 | 236 | g.HistoryCount = store.HistoryCount 237 | 238 | g.FirstHistoryByHeight = store.FirstHistoryByHeight 239 | g.LastHistoryHeight = store.LastHistoryHeight 240 | 241 | // brc20 base 242 | g.AllHistory = store.AllHistory 243 | g.UserAllHistory = store.UserAllHistory 244 | 245 | g.InscriptionsTickerInfoMap = store.InscriptionsTickerInfoMap 246 | g.UserTokensBalanceData = store.UserTokensBalanceData 247 | 248 | // balance 249 | for u, userTokens := range g.UserTokensBalanceData { 250 | for uniqueLowerTicker, balance := range userTokens { 251 | tokenUsers, ok := g.TokenUsersBalanceData[uniqueLowerTicker] 252 | if !ok { 253 | tokenUsers = make(map[string]*model.BRC20TokenBalance, 0) 254 | g.TokenUsersBalanceData[uniqueLowerTicker] = tokenUsers 255 | } 256 | if balance.OverallBalance().Sign() > 0 { 257 | tokenUsers[u] = balance 258 | } 259 | } 260 | } 261 | 262 | g.InscriptionsValidBRC20DataMap = store.InscriptionsValidBRC20DataMap 263 | 264 | // inner valid transfer 265 | g.InscriptionsValidTransferMap = store.InscriptionsValidTransferMap 266 | // inner invalid transfer 267 | g.InscriptionsInvalidTransferMap = store.InscriptionsInvalidTransferMap 268 | 269 | // module 270 | // all modules info 271 | // module of users [address]moduleid 272 | g.UsersModuleWithTokenMap = store.UsersModuleWithTokenMap 273 | 274 | // module lp of users [address]moduleid 275 | g.UsersModuleWithLpTokenMap = store.UsersModuleWithLpTokenMap 276 | 277 | // runtime for approve 278 | g.InscriptionsValidApproveMap = store.InscriptionsValidApproveMap 279 | g.InscriptionsInvalidApproveMap = store.InscriptionsInvalidApproveMap 280 | 281 | // runtime for conditional approve 282 | g.InscriptionsValidConditionalApproveMap = store.InscriptionsValidConditionalApproveMap 283 | g.InscriptionsInvalidConditionalApproveMap = store.InscriptionsInvalidConditionalApproveMap 284 | 285 | // runtime for commit 286 | g.InscriptionsValidCommitMap = store.InscriptionsValidCommitMap 287 | g.InscriptionsInvalidCommitMap = store.InscriptionsInvalidCommitMap 288 | 289 | // InscriptionsValidCommitMapById 290 | for _, v := range g.InscriptionsValidCommitMap { 291 | g.InscriptionsValidCommitMapById[v.GetInscriptionId()] = v 292 | } 293 | 294 | for module, infoStore := range store.ModulesInfoMap { 295 | info := &model.BRC20ModuleSwapInfo{ 296 | ID: infoStore.ID, 297 | Name: infoStore.Name, 298 | DeployerPkScript: infoStore.DeployerPkScript, 299 | SequencerPkScript: infoStore.SequencerPkScript, 300 | GasToPkScript: infoStore.GasToPkScript, 301 | LpFeePkScript: infoStore.LpFeePkScript, 302 | 303 | FeeRateSwap: infoStore.FeeRateSwap, 304 | GasTick: infoStore.GasTick, 305 | 306 | History: infoStore.History, 307 | 308 | // runtime for commit 309 | CommitInvalidMap: infoStore.CommitInvalidMap, 310 | CommitIdMap: infoStore.CommitIdMap, 311 | CommitIdChainMap: infoStore.CommitIdChainMap, 312 | 313 | // token holders in module 314 | // ticker of users in module [address][tick]balanceData 315 | UsersTokenBalanceDataMap: infoStore.UsersTokenBalanceDataMap, 316 | TokenUsersBalanceDataMap: make(map[string]map[string]*model.BRC20ModuleTokenBalance, 0), 317 | 318 | // swap 319 | // lp token balance of address in module [pool][address]balance 320 | LPTokenUsersBalanceMap: infoStore.LPTokenUsersBalanceMap, 321 | UsersLPTokenBalanceMap: make(map[string]map[string]*decimal.Decimal, 0), 322 | 323 | // swap total balance 324 | // total balance of pool in module [pool]balanceData 325 | SwapPoolTotalBalanceDataMap: infoStore.SwapPoolTotalBalanceDataMap, 326 | 327 | // module deposit/withdraw state [tick]balanceData 328 | ConditionalApproveStateBalanceDataMap: infoStore.ConditionalApproveStateBalanceDataMap, 329 | } 330 | 331 | // tick/user: balance 332 | for address, dataMap := range info.UsersTokenBalanceDataMap { 333 | for uniqueLowerTicker, tokenBalance := range dataMap { 334 | tokenUsers, ok := info.TokenUsersBalanceDataMap[uniqueLowerTicker] 335 | if !ok { 336 | tokenUsers = make(map[string]*model.BRC20ModuleTokenBalance, 0) 337 | info.TokenUsersBalanceDataMap[uniqueLowerTicker] = tokenUsers 338 | } 339 | tokenUsers[address] = tokenBalance 340 | } 341 | } 342 | 343 | // pair/user: lpbalance 344 | for pair, dataMap := range info.LPTokenUsersBalanceMap { 345 | for address, lpBalance := range dataMap { 346 | userTokens, ok := info.UsersLPTokenBalanceMap[address] 347 | if !ok { 348 | userTokens = make(map[string]*decimal.Decimal, 0) 349 | info.UsersLPTokenBalanceMap[address] = userTokens 350 | } 351 | userTokens[pair] = lpBalance 352 | } 353 | } 354 | 355 | g.ModulesInfoMap[module] = info 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /loader/dump.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "log" 7 | "os" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/unisat-wallet/libbrc20-indexer/conf" 12 | "github.com/unisat-wallet/libbrc20-indexer/constant" 13 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 14 | "github.com/unisat-wallet/libbrc20-indexer/model" 15 | "github.com/unisat-wallet/libbrc20-indexer/utils" 16 | ) 17 | 18 | func DumpBRC20InputData(fname string, brc20Datas chan interface{}, hexBody bool) { 19 | file, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 20 | if err != nil { 21 | log.Fatalf("open block index file failed, %s", err) 22 | return 23 | } 24 | defer file.Close() 25 | 26 | for dataIn := range brc20Datas { 27 | data := dataIn.(*model.InscriptionBRC20Data) 28 | 29 | var body, address string 30 | if hexBody { 31 | body = hex.EncodeToString(data.ContentBody) 32 | address = hex.EncodeToString([]byte(data.PkScript)) 33 | } else { 34 | body = strings.ReplaceAll(string(data.ContentBody), "\n", " ") 35 | address, err = utils.GetAddressFromScript([]byte(data.PkScript), conf.GlobalNetParams) 36 | if err != nil { 37 | address = hex.EncodeToString([]byte(data.PkScript)) 38 | } 39 | } 40 | 41 | fmt.Fprintf(file, "%d %s %d %d %d %d %s %d %s %s %d %d %d\n", 42 | data.Sequence, 43 | utils.HashString([]byte(data.TxId)), 44 | data.Idx, 45 | data.Vout, 46 | data.Offset, 47 | data.Satoshi, 48 | address, 49 | data.InscriptionNumber, 50 | body, 51 | hex.EncodeToString([]byte(data.CreateIdxKey)), 52 | data.Height, 53 | data.TxIdx, 54 | data.BlockTime, 55 | ) 56 | } 57 | } 58 | 59 | func DumpTickerInfoMap(fname string, 60 | historyData [][]byte, 61 | inscriptionsTickerInfoMap map[string]*model.BRC20TokenInfo, 62 | userTokensBalanceData map[string]map[string]*model.BRC20TokenBalance, 63 | tokenUsersBalanceData map[string]map[string]*model.BRC20TokenBalance, 64 | ) { 65 | 66 | file, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 67 | if err != nil { 68 | log.Fatalf("open block index file failed, %s", err) 69 | return 70 | } 71 | defer file.Close() 72 | 73 | var allTickers []string 74 | for ticker := range inscriptionsTickerInfoMap { 75 | allTickers = append(allTickers, ticker) 76 | } 77 | sort.SliceStable(allTickers, func(i, j int) bool { 78 | return allTickers[i] < allTickers[j] 79 | }) 80 | 81 | for _, ticker := range allTickers { 82 | info := inscriptionsTickerInfoMap[ticker] 83 | nValid := 0 84 | for _, hIdx := range info.History { 85 | buf := historyData[hIdx] 86 | h := &model.BRC20History{} 87 | h.Unmarshal(buf) 88 | 89 | if h.Valid { 90 | nValid++ 91 | } 92 | } 93 | 94 | fmt.Fprintf(file, "%s history: %d, valid: %d, minted: %s, holders: %d\n", 95 | info.Ticker, 96 | len(info.History), 97 | nValid, 98 | info.Deploy.TotalMinted.String(), 99 | len(tokenUsersBalanceData[ticker]), 100 | ) 101 | 102 | // history 103 | for _, hIdx := range info.History { 104 | buf := historyData[hIdx] 105 | h := &model.BRC20History{} 106 | h.Unmarshal(buf) 107 | 108 | if !h.Valid { 109 | continue 110 | } 111 | 112 | addressFrom, err := utils.GetAddressFromScript([]byte(h.PkScriptFrom), conf.GlobalNetParams) 113 | if err != nil { 114 | addressFrom = hex.EncodeToString([]byte(h.PkScriptFrom)) 115 | } 116 | 117 | addressTo, err := utils.GetAddressFromScript([]byte(h.PkScriptTo), conf.GlobalNetParams) 118 | if err != nil { 119 | addressTo = hex.EncodeToString([]byte(h.PkScriptTo)) 120 | } 121 | 122 | fmt.Fprintf(file, "%s %s %s %s %s -> %s\n", 123 | info.Ticker, 124 | utils.HashString([]byte(h.TxId)), 125 | constant.BRC20_HISTORY_TYPE_NAMES[h.Type], 126 | h.Amount, 127 | addressFrom, 128 | addressTo, 129 | ) 130 | } 131 | 132 | // holders 133 | var allHoldersPkScript []string 134 | for holder := range tokenUsersBalanceData[ticker] { 135 | allHoldersPkScript = append(allHoldersPkScript, holder) 136 | } 137 | // sort by holder address 138 | sort.SliceStable(allHoldersPkScript, func(i, j int) bool { 139 | return allHoldersPkScript[i] < allHoldersPkScript[j] 140 | }) 141 | 142 | // holders 143 | for _, holder := range allHoldersPkScript { 144 | balanceData := tokenUsersBalanceData[ticker][holder] 145 | 146 | address, err := utils.GetAddressFromScript([]byte(balanceData.PkScript), conf.GlobalNetParams) 147 | if err != nil { 148 | address = hex.EncodeToString([]byte(balanceData.PkScript)) 149 | } 150 | fmt.Fprintf(file, "%s %s history: %d, transfer: %d, balance: %s, tokens: %d\n", 151 | info.Ticker, 152 | address, 153 | len(balanceData.History), 154 | len(balanceData.ValidTransferMap), 155 | balanceData.OverallBalance().String(), 156 | len(userTokensBalanceData[holder]), 157 | ) 158 | } 159 | } 160 | } 161 | 162 | func DumpModuleInfoMap(fname string, 163 | modulesInfoMap map[string]*model.BRC20ModuleSwapInfo, 164 | ) { 165 | file, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 166 | if err != nil { 167 | log.Fatalf("open module dump file failed, %s", err) 168 | return 169 | } 170 | defer file.Close() 171 | 172 | var allModules []string 173 | for moduleId := range modulesInfoMap { 174 | allModules = append(allModules, moduleId) 175 | } 176 | sort.SliceStable(allModules, func(i, j int) bool { 177 | return allModules[i] < allModules[j] 178 | }) 179 | 180 | for _, moduleId := range allModules { 181 | info := modulesInfoMap[moduleId] 182 | nValid := 0 183 | for _, h := range info.History { 184 | if h.Valid { 185 | nValid++ 186 | } 187 | } 188 | 189 | fmt.Fprintf(file, "module %s(%s) nHistory: %d, nValidHistory: %d, nCommit: %d, nTickers: %d, nHolders: %d, swap: %d, lpholders: %d\n", 190 | info.Name, 191 | info.ID, 192 | len(info.History), 193 | nValid, 194 | len(info.CommitIdChainMap), 195 | len(info.TokenUsersBalanceDataMap), 196 | len(info.UsersTokenBalanceDataMap), 197 | 198 | len(info.LPTokenUsersBalanceMap), 199 | len(info.UsersLPTokenBalanceMap), 200 | ) 201 | 202 | DumpModuleTickInfoMap(file, info.ConditionalApproveStateBalanceDataMap, info.TokenUsersBalanceDataMap, info.UsersTokenBalanceDataMap) 203 | 204 | DumpModuleSwapInfoMap(file, info.SwapPoolTotalBalanceDataMap, info.LPTokenUsersBalanceMap, info.UsersLPTokenBalanceMap) 205 | } 206 | } 207 | 208 | func DumpModuleTickInfoMap(file *os.File, condStateBalanceDataMap map[string]*model.BRC20ModuleConditionalApproveStateBalance, 209 | inscriptionsTickerInfoMap, userTokensBalanceData map[string]map[string]*model.BRC20ModuleTokenBalance, 210 | ) { 211 | 212 | var allTickers []string 213 | for ticker := range inscriptionsTickerInfoMap { 214 | allTickers = append(allTickers, ticker) 215 | } 216 | sort.SliceStable(allTickers, func(i, j int) bool { 217 | return allTickers[i] < allTickers[j] 218 | }) 219 | 220 | for _, ticker := range allTickers { 221 | holdersMap := inscriptionsTickerInfoMap[ticker] 222 | 223 | nHistory := 0 224 | nValid := 0 225 | 226 | var allHoldersPkScript []string 227 | for holder, data := range holdersMap { 228 | nHistory += len(data.History) 229 | for _, h := range data.History { 230 | if h.Valid { 231 | nValid++ 232 | } 233 | } 234 | allHoldersPkScript = append(allHoldersPkScript, holder) 235 | } 236 | sort.SliceStable(allHoldersPkScript, func(i, j int) bool { 237 | return allHoldersPkScript[i] < allHoldersPkScript[j] 238 | }) 239 | 240 | fmt.Fprintf(file, " %s nHistory: %d, valid: %d, nHolders: %d\n", 241 | ticker, 242 | nHistory, 243 | nValid, 244 | // TokenTotalBalance[tick], // fixme 245 | len(holdersMap), 246 | ) 247 | 248 | // holders 249 | for _, holder := range allHoldersPkScript { 250 | balanceData := holdersMap[holder] 251 | 252 | address, err := utils.GetAddressFromScript([]byte(balanceData.PkScript), conf.GlobalNetParams) 253 | if err != nil { 254 | address = hex.EncodeToString([]byte(balanceData.PkScript)) 255 | } 256 | fmt.Fprintf(file, " %s %s nHistory: %d, bnModule: %s, bnAvai: %s, bnSwap: %s, bnCond: %s, nToken: %d", 257 | ticker, 258 | address, 259 | len(balanceData.History), 260 | balanceData.ModuleBalance().String(), 261 | balanceData.AvailableBalance.String(), 262 | balanceData.SwapAccountBalance.String(), 263 | balanceData.CondApproveableBalance.String(), 264 | len(userTokensBalanceData[string(balanceData.PkScript)]), 265 | ) 266 | 267 | if len(balanceData.ValidApproveMap) > 0 { 268 | fmt.Fprintf(file, ", nApprove: %d", len(balanceData.ValidApproveMap)) 269 | } 270 | if len(balanceData.ReadyToWithdrawMap) > 0 { 271 | fmt.Fprintf(file, ", nWithdraw: %d", len(balanceData.ReadyToWithdrawMap)) 272 | } 273 | fmt.Fprintf(file, "\n") 274 | } 275 | } 276 | 277 | fmt.Fprintf(file, "\n") 278 | 279 | // condStateBalanceDataMap 280 | for _, ticker := range allTickers { 281 | stateBalance, ok := condStateBalanceDataMap[ticker] 282 | if !ok { 283 | fmt.Fprintf(file, " module deposit/withdraw state: %s - \n", ticker) 284 | continue 285 | } 286 | 287 | fmt.Fprintf(file, " module deposit/withdraw state: %s deposit: %s, match: %s, new: %s, cancel: %s, wait: %s\n", 288 | ticker, 289 | stateBalance.BalanceDeposite.String(), 290 | stateBalance.BalanceApprove.String(), 291 | stateBalance.BalanceNewApprove.String(), 292 | stateBalance.BalanceCancelApprove.String(), 293 | 294 | stateBalance.BalanceNewApprove.Sub( 295 | stateBalance.BalanceApprove).Sub( 296 | stateBalance.BalanceCancelApprove).String(), 297 | ) 298 | } 299 | 300 | fmt.Fprintf(file, "\n") 301 | } 302 | 303 | func DumpModuleSwapInfoMap(file *os.File, 304 | swapPoolTotalBalanceDataMap map[string]*model.BRC20ModulePoolTotalBalance, 305 | inscriptionsTickerInfoMap, userTokensBalanceData map[string]map[string]*decimal.Decimal) { 306 | 307 | var allTickers []string 308 | for ticker := range inscriptionsTickerInfoMap { 309 | allTickers = append(allTickers, ticker) 310 | } 311 | sort.SliceStable(allTickers, func(i, j int) bool { 312 | return allTickers[i] < allTickers[j] 313 | }) 314 | 315 | for _, ticker := range allTickers { 316 | holdersMap := inscriptionsTickerInfoMap[ticker] 317 | 318 | var allHoldersPkScript []string 319 | for holder := range holdersMap { 320 | allHoldersPkScript = append(allHoldersPkScript, holder) 321 | } 322 | sort.SliceStable(allHoldersPkScript, func(i, j int) bool { 323 | return allHoldersPkScript[i] < allHoldersPkScript[j] 324 | }) 325 | 326 | swap := swapPoolTotalBalanceDataMap[ticker] 327 | 328 | fmt.Fprintf(file, " pool: %s nHistory: %d, nLPholders: %d, lp: %s, %s: %s, %s: %s\n", 329 | ticker, 330 | len(swap.History), 331 | len(holdersMap), 332 | swap.LpBalance, 333 | swap.Tick[0], 334 | swap.TickBalance[0], 335 | swap.Tick[1], 336 | swap.TickBalance[1], 337 | ) 338 | 339 | // holders 340 | for _, holder := range allHoldersPkScript { 341 | balanceData := holdersMap[holder] 342 | 343 | address, err := utils.GetAddressFromScript([]byte(holder), conf.GlobalNetParams) 344 | if err != nil { 345 | address = hex.EncodeToString([]byte(holder)) 346 | } 347 | fmt.Fprintf(file, " pool: %s %s lp: %s, swaps: %d\n", 348 | ticker, 349 | address, 350 | balanceData.String(), 351 | len(userTokensBalanceData[holder]), 352 | ) 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /loader/load_from_data.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bufio" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/unisat-wallet/libbrc20-indexer/model" 12 | "github.com/unisat-wallet/libbrc20-indexer/utils" 13 | ) 14 | 15 | func LoadBRC20InputData(fname string, brc20Datas chan interface{}) error { 16 | file, err := os.Open(fname) 17 | if err != nil { 18 | return err 19 | } 20 | defer file.Close() 21 | 22 | scanner := bufio.NewScanner(file) 23 | max := 128 * 1024 * 1024 24 | buf := make([]byte, max) 25 | scanner.Buffer(buf, max) 26 | 27 | for scanner.Scan() { 28 | line := scanner.Text() 29 | fields := strings.Split(line, " ") 30 | 31 | if len(fields) != 13 { 32 | return fmt.Errorf("invalid data format") 33 | } 34 | 35 | var data model.InscriptionBRC20Data 36 | 37 | sequence, err := strconv.ParseUint(fields[0], 10, 16) 38 | if err != nil { 39 | return err 40 | } 41 | data.Sequence = uint16(sequence) 42 | data.IsTransfer = (data.Sequence > 0) 43 | 44 | txid, err := hex.DecodeString(fields[1]) 45 | if err != nil { 46 | return err 47 | } 48 | data.TxId = string(utils.ReverseBytes([]byte(txid))) 49 | 50 | idx, err := strconv.ParseUint(fields[2], 10, 32) 51 | if err != nil { 52 | return err 53 | } 54 | data.Idx = uint32(idx) 55 | 56 | vout, err := strconv.ParseUint(fields[3], 10, 32) 57 | if err != nil { 58 | return err 59 | } 60 | data.Vout = uint32(vout) 61 | 62 | offset, err := strconv.ParseUint(fields[4], 10, 64) 63 | if err != nil { 64 | return err 65 | } 66 | data.Offset = uint64(offset) 67 | 68 | satoshi, err := strconv.ParseUint(fields[5], 10, 64) 69 | if err != nil { 70 | return err 71 | } 72 | data.Satoshi = uint64(satoshi) 73 | 74 | pkScript, err := hex.DecodeString(fields[6]) 75 | if err != nil { 76 | return err 77 | } 78 | data.PkScript = string(pkScript) 79 | 80 | inscriptionNumber, err := strconv.ParseInt(fields[7], 10, 64) 81 | if err != nil { 82 | return err 83 | } 84 | data.InscriptionNumber = int64(inscriptionNumber) 85 | 86 | content, err := hex.DecodeString(fields[8]) 87 | if err != nil { 88 | return err 89 | } 90 | data.ContentBody = content 91 | 92 | createIdxKey, err := hex.DecodeString(fields[9]) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | data.CreateIdxKey = string(createIdxKey) 98 | 99 | height, err := strconv.ParseUint(fields[10], 10, 32) 100 | if err != nil { 101 | return err 102 | } 103 | data.Height = uint32(height) 104 | 105 | txIdx, err := strconv.ParseUint(fields[11], 10, 32) 106 | if err != nil { 107 | return err 108 | } 109 | data.TxIdx = uint32(txIdx) 110 | 111 | blockTime, err := strconv.ParseUint(fields[12], 10, 32) 112 | if err != nil { 113 | return err 114 | } 115 | data.BlockTime = uint32(blockTime) 116 | 117 | brc20Datas <- &data 118 | } 119 | 120 | if err := scanner.Err(); err != nil { 121 | return err 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /loader/load_from_json.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bufio" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/unisat-wallet/libbrc20-indexer/model" 12 | "github.com/unisat-wallet/libbrc20-indexer/utils" 13 | ) 14 | 15 | func LoadBRC20InputJsonData(fname string) ([]*model.InscriptionBRC20Data, error) { 16 | file, err := os.Open(fname) 17 | if err != nil { 18 | return nil, err 19 | } 20 | defer file.Close() 21 | 22 | var brc20Datas []*model.InscriptionBRC20Data 23 | scanner := bufio.NewScanner(file) 24 | 25 | for scanner.Scan() { 26 | line := scanner.Text() 27 | 28 | if len(line) == 0 { 29 | continue 30 | } 31 | if strings.HasPrefix(line, "#") { 32 | continue 33 | } 34 | 35 | fields := strings.Split(line, " ") 36 | 37 | if len(fields) != 13 { 38 | return nil, fmt.Errorf("invalid data format") 39 | } 40 | 41 | var data model.InscriptionBRC20Data 42 | data.IsTransfer, err = strconv.ParseBool(fields[0]) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | txid, err := hex.DecodeString(fields[1]) 48 | if err != nil { 49 | return nil, err 50 | } 51 | data.TxId = string(utils.ReverseBytes([]byte(txid))) 52 | 53 | idx, err := strconv.ParseUint(fields[2], 10, 32) 54 | if err != nil { 55 | return nil, err 56 | } 57 | data.Idx = uint32(idx) 58 | 59 | vout, err := strconv.ParseUint(fields[3], 10, 32) 60 | if err != nil { 61 | return nil, err 62 | } 63 | data.Vout = uint32(vout) 64 | 65 | offset, err := strconv.ParseUint(fields[4], 10, 64) 66 | if err != nil { 67 | return nil, err 68 | } 69 | data.Offset = uint64(offset) 70 | 71 | satoshi, err := strconv.ParseUint(fields[5], 10, 64) 72 | if err != nil { 73 | return nil, err 74 | } 75 | data.Satoshi = uint64(satoshi) 76 | 77 | pkScript, err := hex.DecodeString(fields[6]) 78 | if err != nil { 79 | return nil, err 80 | } 81 | data.PkScript = string(pkScript) 82 | 83 | inscriptionNumber, err := strconv.ParseInt(fields[7], 10, 64) 84 | if err != nil { 85 | return nil, err 86 | } 87 | data.InscriptionNumber = int64(inscriptionNumber) 88 | 89 | data.ContentBody = []byte(fields[8]) 90 | 91 | createIdxKey, err := hex.DecodeString(fields[9]) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | data.CreateIdxKey = string(createIdxKey) 97 | 98 | height, err := strconv.ParseUint(fields[10], 10, 32) 99 | if err != nil { 100 | return nil, err 101 | } 102 | data.Height = uint32(height) 103 | 104 | txIdx, err := strconv.ParseUint(fields[11], 10, 32) 105 | if err != nil { 106 | return nil, err 107 | } 108 | data.TxIdx = uint32(txIdx) 109 | 110 | blockTime, err := strconv.ParseUint(fields[12], 10, 32) 111 | if err != nil { 112 | return nil, err 113 | } 114 | data.BlockTime = uint32(blockTime) 115 | 116 | brc20Datas = append(brc20Datas, &data) 117 | } 118 | 119 | if err := scanner.Err(); err != nil { 120 | return nil, err 121 | } 122 | 123 | return brc20Datas, nil 124 | } 125 | -------------------------------------------------------------------------------- /model/conditional_approve.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 5 | ) 6 | 7 | // state of approve for each tick, (balance and history) 8 | type BRC20ModuleConditionalApproveStateBalance struct { 9 | Tick string 10 | BalanceDeposite *decimal.Decimal // Direct Charging Total amount 11 | 12 | BalanceApprove *decimal.Decimal // Total amount of successful withdrawal matches 13 | BalanceNewApprove *decimal.Decimal // Initiate total withdrawal amount 14 | BalanceCancelApprove *decimal.Decimal // Total amount of withdrawals cancelled 15 | BalanceWaitApprove *decimal.Decimal // Total amount waiting for withdrawal matching 16 | 17 | // BalanceNewApprove - BalanceCancelApprove - BalanceApprove == BalanceWaitApprove 18 | } 19 | 20 | func (in *BRC20ModuleConditionalApproveStateBalance) DeepCopy() *BRC20ModuleConditionalApproveStateBalance { 21 | tb := &BRC20ModuleConditionalApproveStateBalance{ 22 | Tick: in.Tick, 23 | BalanceDeposite: decimal.NewDecimalCopy(in.BalanceDeposite), 24 | 25 | BalanceApprove: decimal.NewDecimalCopy(in.BalanceApprove), 26 | BalanceNewApprove: decimal.NewDecimalCopy(in.BalanceNewApprove), 27 | BalanceCancelApprove: decimal.NewDecimalCopy(in.BalanceCancelApprove), 28 | BalanceWaitApprove: decimal.NewDecimalCopy(in.BalanceWaitApprove), 29 | } 30 | return tb 31 | } 32 | -------------------------------------------------------------------------------- /model/history.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | scriptDecoder "github.com/unisat-wallet/libbrc20-indexer/utils/script" 7 | ) 8 | 9 | type BRC20HistoryBase struct { 10 | Type uint8 // inscribe-deploy/inscribe-mint/inscribe-transfer/transfer/send/receive 11 | Valid bool 12 | 13 | TxId string 14 | Idx uint32 15 | Vout uint32 16 | Offset uint64 17 | 18 | PkScriptFrom string 19 | PkScriptTo string 20 | Satoshi uint64 21 | Fee int64 22 | 23 | Height uint32 24 | TxIdx uint32 25 | BlockTime uint32 26 | } 27 | 28 | // history 29 | type BRC20History struct { 30 | BRC20HistoryBase 31 | 32 | Inscription InscriptionBRC20TickInfoResp 33 | 34 | // param 35 | Amount string 36 | 37 | // state 38 | OverallBalance string 39 | TransferableBalance string 40 | AvailableBalance string 41 | } 42 | 43 | func NewBRC20History(historyType uint8, isValid bool, isTransfer bool, 44 | from *InscriptionBRC20TickInfo, bal *BRC20TokenBalance, to *InscriptionBRC20Data) *BRC20History { 45 | history := &BRC20History{ 46 | BRC20HistoryBase: BRC20HistoryBase{ 47 | Type: historyType, 48 | Valid: isValid, 49 | Height: to.Height, 50 | TxIdx: to.TxIdx, 51 | BlockTime: to.BlockTime, 52 | Fee: to.Fee, 53 | }, 54 | Inscription: InscriptionBRC20TickInfoResp{ 55 | Height: from.Height, 56 | Data: from.Data, 57 | InscriptionNumber: from.InscriptionNumber, 58 | InscriptionId: from.GetInscriptionId(), 59 | Satoshi: from.Satoshi, 60 | }, 61 | Amount: from.Amount.String(), 62 | } 63 | if isTransfer { 64 | history.TxId = to.TxId 65 | history.Vout = to.Vout 66 | history.Offset = to.Offset 67 | history.Idx = to.Idx 68 | history.PkScriptFrom = from.PkScript 69 | history.PkScriptTo = to.PkScript 70 | history.Satoshi = to.Satoshi 71 | if history.Satoshi == 0 { 72 | history.PkScriptTo = history.PkScriptFrom 73 | } 74 | 75 | } else { 76 | history.TxId = from.TxId 77 | history.Vout = from.Vout 78 | history.Offset = from.Offset 79 | history.Idx = from.Idx 80 | history.PkScriptTo = from.PkScript 81 | history.Satoshi = from.Satoshi 82 | } 83 | 84 | if bal != nil { 85 | history.OverallBalance = bal.AvailableBalance.Add(bal.TransferableBalance).String() 86 | history.TransferableBalance = bal.TransferableBalance.String() 87 | history.AvailableBalance = bal.AvailableBalance.String() 88 | } else { 89 | history.OverallBalance = "0" 90 | history.TransferableBalance = "0" 91 | history.AvailableBalance = "0" 92 | } 93 | return history 94 | } 95 | 96 | func (h *BRC20History) Marshal() (result []byte) { 97 | var buf [1024]byte 98 | 99 | // type 100 | buf[0] = h.Type 101 | // valid 102 | if h.Valid { 103 | buf[1] = 1 104 | } else { 105 | buf[1] = 0 106 | } 107 | // txid 108 | copy(buf[2:2+32], h.TxId[:]) 109 | 110 | offset := 34 111 | 112 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Idx)) 113 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Vout)) 114 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Offset)) 115 | 116 | offset += scriptDecoder.PutCompressedScript(buf[offset:], []byte(h.PkScriptFrom)) 117 | offset += scriptDecoder.PutCompressedScript(buf[offset:], []byte(h.PkScriptTo)) 118 | 119 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Satoshi)) 120 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Fee)) 121 | 122 | binary.LittleEndian.PutUint32(buf[offset:], h.Height) // 4 123 | offset += 4 124 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.TxIdx)) 125 | binary.LittleEndian.PutUint32(buf[offset:], h.BlockTime) // 4 126 | offset += 4 127 | 128 | // Amount 129 | n := len(h.Amount) 130 | if n < 40 { 131 | buf[offset] = uint8(n) 132 | offset += 1 133 | copy(buf[offset:offset+n], h.Amount[:]) 134 | offset += n 135 | } else { 136 | buf[offset] = 0 137 | offset += 1 138 | } 139 | 140 | // OverallBalance 141 | n = len(h.OverallBalance) 142 | if n < 40 { 143 | buf[offset] = uint8(n) 144 | offset += 1 145 | copy(buf[offset:offset+n], h.OverallBalance[:]) 146 | offset += n 147 | } else { 148 | buf[offset] = 0 149 | offset += 1 150 | } 151 | 152 | // TransferableBalance 153 | n = len(h.TransferableBalance) 154 | if n < 40 { 155 | buf[offset] = uint8(n) 156 | offset += 1 157 | copy(buf[offset:offset+n], h.TransferableBalance[:]) 158 | offset += n 159 | } else { 160 | buf[offset] = 0 161 | offset += 1 162 | } 163 | 164 | // AvailableBalance 165 | n = len(h.AvailableBalance) 166 | if n < 40 { 167 | buf[offset] = uint8(n) 168 | offset += 1 169 | copy(buf[offset:offset+n], h.AvailableBalance[:]) 170 | offset += n 171 | } else { 172 | buf[offset] = 0 173 | offset += 1 174 | } 175 | 176 | // Inscription 177 | binary.LittleEndian.PutUint32(buf[offset:], h.Inscription.Height) // 4 178 | offset += 4 179 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Inscription.InscriptionNumber)) 180 | offset += scriptDecoder.PutVLQ(buf[offset:], uint64(h.Inscription.Satoshi)) 181 | // inscriptionId 182 | n = len(h.Inscription.InscriptionId) 183 | if n < 70 { 184 | buf[offset] = uint8(n) 185 | offset += 1 186 | copy(buf[offset:offset+n], h.Inscription.InscriptionId[:]) 187 | offset += n 188 | } else { 189 | buf[offset] = 0 190 | offset += 1 191 | } 192 | 193 | // data 194 | data := h.Inscription.Data 195 | if data == nil { 196 | result = make([]byte, offset) 197 | copy(result, buf[:offset]) 198 | return result 199 | } 200 | 201 | // BRC20Tick 202 | n = len(data.BRC20Tick) 203 | if n < 16 { 204 | buf[offset] = uint8(n) 205 | offset += 1 206 | copy(buf[offset:offset+n], data.BRC20Tick[:]) 207 | offset += n 208 | } else { 209 | buf[offset] = 0 210 | offset += 1 211 | } 212 | 213 | // BRC20Max 214 | n = len(data.BRC20Max) 215 | if n < 40 { 216 | buf[offset] = uint8(n) 217 | offset += 1 218 | copy(buf[offset:offset+n], data.BRC20Max[:]) 219 | offset += n 220 | } else { 221 | buf[offset] = 0 222 | offset += 1 223 | } 224 | // BRC20Limit 225 | n = len(data.BRC20Limit) 226 | if n < 40 { 227 | buf[offset] = uint8(n) 228 | offset += 1 229 | copy(buf[offset:offset+n], data.BRC20Limit[:]) 230 | offset += n 231 | } else { 232 | buf[offset] = 0 233 | offset += 1 234 | } 235 | // BRC20Amount 236 | n = len(data.BRC20Amount) 237 | if n < 40 { 238 | buf[offset] = uint8(n) 239 | offset += 1 240 | copy(buf[offset:offset+n], data.BRC20Amount[:]) 241 | offset += n 242 | } else { 243 | buf[offset] = 0 244 | offset += 1 245 | } 246 | // BRC20Decimal 247 | n = len(data.BRC20Decimal) 248 | if n < 8 { 249 | buf[offset] = uint8(n) 250 | offset += 1 251 | copy(buf[offset:offset+n], data.BRC20Decimal[:]) 252 | offset += n 253 | } else { 254 | buf[offset] = 0 255 | offset += 1 256 | } 257 | // BRC20Minted 258 | n = len(data.BRC20Minted) 259 | if n < 40 { 260 | buf[offset] = uint8(n) 261 | offset += 1 262 | copy(buf[offset:offset+n], data.BRC20Minted[:]) 263 | offset += n 264 | } else { 265 | buf[offset] = 0 266 | offset += 1 267 | } 268 | // BRC20SelfMint 269 | n = len(data.BRC20SelfMint) 270 | if n < 8 { 271 | buf[offset] = uint8(n) 272 | offset += 1 273 | copy(buf[offset:offset+n], data.BRC20SelfMint[:]) 274 | offset += n 275 | } else { 276 | buf[offset] = 0 277 | offset += 1 278 | } 279 | result = make([]byte, offset) 280 | copy(result, buf[:offset]) 281 | return result 282 | } 283 | 284 | func (h *BRC20History) Unmarshal(buf []byte) { 285 | h.Type = buf[0] 286 | h.Valid = (buf[1] == 1) 287 | 288 | h.TxId = string(buf[2 : 2+32]) 289 | 290 | offset := 34 291 | 292 | idx, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 293 | if bytesRead >= len(buf[offset:]) { 294 | return 295 | } 296 | h.Idx = uint32(idx) 297 | offset += bytesRead 298 | 299 | vout, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 300 | if bytesRead >= len(buf[offset:]) { 301 | return 302 | } 303 | h.Vout = uint32(vout) 304 | offset += bytesRead 305 | 306 | nftOffset, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 307 | if bytesRead >= len(buf[offset:]) { 308 | return 309 | } 310 | h.Offset = nftOffset 311 | offset += bytesRead 312 | 313 | // Decode the compressed script size and ensure there are enough bytes 314 | // left in the slice for it. 315 | scriptSize := scriptDecoder.DecodeCompressedScriptSize(buf[offset:]) 316 | if len(buf[offset:]) < scriptSize { 317 | return 318 | } 319 | h.PkScriptFrom = string(scriptDecoder.DecompressScript(buf[offset : offset+scriptSize])) 320 | offset += scriptSize 321 | 322 | scriptSize = scriptDecoder.DecodeCompressedScriptSize(buf[offset:]) 323 | if len(buf[offset:]) < scriptSize { 324 | return 325 | } 326 | h.PkScriptTo = string(scriptDecoder.DecompressScript(buf[offset : offset+scriptSize])) 327 | offset += scriptSize 328 | 329 | satoshi, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 330 | if bytesRead >= len(buf[offset:]) { 331 | return 332 | } 333 | h.Satoshi = satoshi 334 | offset += bytesRead 335 | 336 | fee, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 337 | if bytesRead >= len(buf[offset:]) { 338 | return 339 | } 340 | h.Fee = int64(fee) 341 | offset += bytesRead 342 | 343 | h.Height = binary.LittleEndian.Uint32(buf[offset:]) // 4 344 | offset += 4 345 | 346 | txidx, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 347 | if bytesRead >= len(buf[offset:]) { 348 | return 349 | } 350 | h.TxIdx = uint32(txidx) 351 | offset += bytesRead 352 | 353 | h.BlockTime = binary.LittleEndian.Uint32(buf[offset:]) // 4 354 | offset += 4 355 | 356 | // Amount 357 | n := int(buf[offset]) 358 | offset += 1 359 | if n > 0 { 360 | h.Amount = string(buf[offset : offset+n]) 361 | offset += n 362 | } 363 | 364 | // OverallBalance 365 | n = int(buf[offset]) 366 | offset += 1 367 | if n > 0 { 368 | h.OverallBalance = string(buf[offset : offset+n]) 369 | offset += n 370 | } 371 | 372 | // TransferableBalance 373 | n = int(buf[offset]) 374 | offset += 1 375 | if n > 0 { 376 | h.TransferableBalance = string(buf[offset : offset+n]) 377 | offset += n 378 | } 379 | 380 | // AvailableBalance 381 | n = int(buf[offset]) 382 | offset += 1 383 | if n > 0 { 384 | h.AvailableBalance = string(buf[offset : offset+n]) 385 | offset += n 386 | } 387 | 388 | // Inscription 389 | h.Inscription.Height = binary.LittleEndian.Uint32(buf[offset:]) // 4 390 | offset += 4 391 | 392 | number, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 393 | if bytesRead >= len(buf[offset:]) { 394 | return 395 | } 396 | h.Inscription.InscriptionNumber = int64(number) 397 | offset += bytesRead 398 | 399 | nftSatoshi, bytesRead := scriptDecoder.DeserializeVLQ(buf[offset:]) 400 | if bytesRead >= len(buf[offset:]) { 401 | return 402 | } 403 | h.Inscription.Satoshi = nftSatoshi 404 | offset += bytesRead 405 | 406 | // inscriptionId 407 | n = int(buf[offset]) 408 | offset += 1 409 | if n > 0 { 410 | h.Inscription.InscriptionId = string(buf[offset : offset+n]) 411 | offset += n 412 | } 413 | 414 | // data 415 | if len(buf[offset:]) == 0 { 416 | return 417 | } 418 | data := &InscriptionBRC20InfoResp{} 419 | h.Inscription.Data = data 420 | 421 | // BRC20Tick 422 | n = int(buf[offset]) 423 | offset += 1 424 | if n > 0 { 425 | data.BRC20Tick = string(buf[offset : offset+n]) 426 | offset += n 427 | } 428 | 429 | // BRC20Max 430 | n = int(buf[offset]) 431 | offset += 1 432 | if n > 0 { 433 | data.BRC20Max = string(buf[offset : offset+n]) 434 | offset += n 435 | } 436 | 437 | // BRC20Limit 438 | n = int(buf[offset]) 439 | offset += 1 440 | if n > 0 { 441 | data.BRC20Limit = string(buf[offset : offset+n]) 442 | offset += n 443 | } 444 | 445 | // BRC20Amount 446 | n = int(buf[offset]) 447 | offset += 1 448 | if n > 0 { 449 | data.BRC20Amount = string(buf[offset : offset+n]) 450 | offset += n 451 | } 452 | 453 | // BRC20Decimal 454 | n = int(buf[offset]) 455 | offset += 1 456 | if n > 0 { 457 | data.BRC20Decimal = string(buf[offset : offset+n]) 458 | offset += n 459 | } 460 | 461 | // BRC20Minted 462 | n = int(buf[offset]) 463 | offset += 1 464 | if n > 0 { 465 | data.BRC20Minted = string(buf[offset : offset+n]) 466 | offset += n 467 | } 468 | 469 | // BRC20SelfMint 470 | n = int(buf[offset]) 471 | offset += 1 472 | if n > 0 { 473 | data.BRC20SelfMint = string(buf[offset : offset+n]) 474 | offset += n 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /model/history_module.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // history 4 | type BRC20ModuleHistory struct { 5 | BRC20HistoryBase 6 | Inscription InscriptionBRC20SwapInfoResp 7 | Data any 8 | // no state 9 | } 10 | 11 | func NewBRC20ModuleHistory(isTransfer bool, historyType uint8, from, to *InscriptionBRC20Data, data any, isValid bool) *BRC20ModuleHistory { 12 | history := &BRC20ModuleHistory{ 13 | BRC20HistoryBase: BRC20HistoryBase{ 14 | Type: historyType, 15 | Valid: isValid, 16 | }, 17 | Inscription: InscriptionBRC20SwapInfoResp{ 18 | Height: from.Height, 19 | ContentBody: from.ContentBody, // to.Content is empty on transfer 20 | InscriptionNumber: from.InscriptionNumber, 21 | InscriptionId: from.GetInscriptionId(), 22 | }, 23 | } 24 | if isTransfer { 25 | history.TxId = to.TxId 26 | history.Vout = to.Vout 27 | history.Offset = to.Offset 28 | history.Idx = to.Idx 29 | history.PkScriptFrom = from.PkScript 30 | history.PkScriptTo = to.PkScript 31 | history.Satoshi = to.Satoshi 32 | 33 | history.Height = to.Height 34 | history.TxIdx = to.TxIdx 35 | history.BlockTime = to.BlockTime 36 | 37 | } else { 38 | history.TxId = from.TxId 39 | history.Vout = from.Vout 40 | history.Offset = from.Offset 41 | history.Idx = from.Idx 42 | history.PkScriptTo = from.PkScript 43 | history.Satoshi = from.Satoshi 44 | 45 | history.Height = from.Height 46 | history.TxIdx = from.TxIdx 47 | history.BlockTime = from.BlockTime 48 | } 49 | history.Data = data 50 | return history 51 | } 52 | 53 | // withdraw history 54 | type BRC20SwapHistoryWithdrawData struct { 55 | Tick string `json:"tick"` 56 | Amount string `json:"amount"` // current amt 57 | } 58 | 59 | // approve history 60 | type BRC20SwapHistoryApproveData struct { 61 | Tick string `json:"tick"` 62 | Amount string `json:"amount"` // current amt 63 | } 64 | 65 | // cond approve history 66 | type BRC20SwapHistoryCondApproveData struct { 67 | Tick string `json:"tick"` 68 | Amount string `json:"amount"` // current amt 69 | Balance string `json:"balance"` // current balance 70 | TransferInscriptionId string `json:"transfer"` // transfer inscription id 71 | TransferMax string `json:"transferMax"` // transfer inscription id 72 | } 73 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/unisat-wallet/libbrc20-indexer/decimal" 9 | "github.com/unisat-wallet/libbrc20-indexer/utils" 10 | ) 11 | 12 | // nft create point on create 13 | type NFTCreateIdxKey struct { 14 | Height uint32 // Height of NFT show in block onCreate 15 | IdxInBlock uint64 // Index of NFT show in block onCreate 16 | } 17 | 18 | func (p *NFTCreateIdxKey) String() string { 19 | var key [12]byte 20 | binary.LittleEndian.PutUint32(key[0:4], p.Height) 21 | binary.LittleEndian.PutUint64(key[4:12], p.IdxInBlock) 22 | return string(key[:]) 23 | } 24 | 25 | // event raw data 26 | type InscriptionBRC20Data struct { 27 | IsTransfer bool 28 | TxId string `json:"-"` 29 | Idx uint32 `json:"-"` 30 | Vout uint32 `json:"-"` 31 | Offset uint64 `json:"-"` 32 | 33 | Satoshi uint64 `json:"-"` 34 | PkScript string `json:"-"` 35 | Fee int64 `json:"-"` 36 | 37 | InscriptionNumber int64 38 | Parent []byte 39 | ContentBody []byte 40 | CreateIdxKey string 41 | 42 | Height uint32 // Height of NFT show in block onCreate 43 | TxIdx uint32 44 | BlockTime uint32 45 | Sequence uint16 46 | 47 | // for cache 48 | InscriptionId string 49 | } 50 | 51 | func (data *InscriptionBRC20Data) GetInscriptionId() string { 52 | if data.InscriptionId == "" { 53 | data.InscriptionId = fmt.Sprintf("%si%d", utils.HashString([]byte(data.TxId)), data.Idx) 54 | } 55 | return data.InscriptionId 56 | } 57 | 58 | type InscriptionBRC20InfoResp struct { 59 | Operation string `json:"op,omitempty"` 60 | BRC20Tick string `json:"tick,omitempty"` 61 | BRC20Max string `json:"max,omitempty"` 62 | BRC20Limit string `json:"lim,omitempty"` 63 | BRC20Amount string `json:"amt,omitempty"` 64 | BRC20Decimal string `json:"decimal,omitempty"` 65 | BRC20Minted string `json:"minted,omitempty"` 66 | BRC20SelfMint string `json:"self_mint,omitempty"` 67 | } 68 | 69 | // decode protocal 70 | type InscriptionBRC20ProtocalContent struct { 71 | Proto string `json:"p,omitempty"` 72 | Operation string `json:"op,omitempty"` 73 | } 74 | 75 | func (body *InscriptionBRC20ProtocalContent) Unmarshal(contentBody []byte) (err error) { 76 | var bodyMap map[string]interface{} = make(map[string]interface{}, 8) 77 | if err := json.Unmarshal(contentBody, &bodyMap); err != nil { 78 | return err 79 | } 80 | if v, ok := bodyMap["p"].(string); ok { 81 | body.Proto = v 82 | } 83 | if v, ok := bodyMap["op"].(string); ok { 84 | body.Operation = v 85 | } 86 | return nil 87 | } 88 | 89 | // decode mint/transfer 90 | type InscriptionBRC20MintTransferContent struct { 91 | Proto string `json:"p,omitempty"` 92 | Operation string `json:"op,omitempty"` 93 | BRC20Tick string `json:"tick,omitempty"` 94 | BRC20Amount string `json:"amt,omitempty"` 95 | } 96 | 97 | func (body *InscriptionBRC20MintTransferContent) Unmarshal(contentBody []byte) (err error) { 98 | var bodyMap map[string]interface{} = make(map[string]interface{}, 8) 99 | if err := json.Unmarshal(contentBody, &bodyMap); err != nil { 100 | return err 101 | } 102 | if v, ok := bodyMap["p"].(string); ok { 103 | body.Proto = v 104 | } 105 | if v, ok := bodyMap["op"].(string); ok { 106 | body.Operation = v 107 | } 108 | if v, ok := bodyMap["tick"].(string); ok { 109 | body.BRC20Tick = v 110 | } 111 | if v, ok := bodyMap["amt"].(string); ok { 112 | body.BRC20Amount = v 113 | } 114 | return nil 115 | } 116 | 117 | // decode deploy data 118 | type InscriptionBRC20DeployContent struct { 119 | Proto string `json:"p,omitempty"` 120 | Operation string `json:"op,omitempty"` 121 | BRC20Tick string `json:"tick,omitempty"` 122 | BRC20Max string `json:"max,omitempty"` 123 | BRC20Limit string `json:"lim,omitempty"` 124 | BRC20Decimal string `json:"dec,omitempty"` 125 | BRC20SelfMint string `json:"self_mint,omitempty"` 126 | } 127 | 128 | func (body *InscriptionBRC20DeployContent) Unmarshal(contentBody []byte) (err error) { 129 | var bodyMap map[string]interface{} = make(map[string]interface{}, 8) 130 | if err := json.Unmarshal(contentBody, &bodyMap); err != nil { 131 | return err 132 | } 133 | if v, ok := bodyMap["p"].(string); ok { 134 | body.Proto = v 135 | } 136 | if v, ok := bodyMap["op"].(string); ok { 137 | body.Operation = v 138 | } 139 | if v, ok := bodyMap["tick"].(string); ok { 140 | body.BRC20Tick = v 141 | } 142 | if _, ok := bodyMap["self_mint"]; ok { // has self_mint 143 | body.BRC20SelfMint = "false" 144 | } 145 | if v, ok := bodyMap["self_mint"].(string); ok { // self_mint is string 146 | body.BRC20SelfMint = v 147 | } 148 | if v, ok := bodyMap["max"].(string); ok { 149 | body.BRC20Max = v 150 | } 151 | if _, ok := bodyMap["lim"]; !ok { 152 | body.BRC20Limit = body.BRC20Max 153 | } else { 154 | if v, ok := bodyMap["lim"].(string); ok { 155 | body.BRC20Limit = v 156 | } 157 | } 158 | 159 | if _, ok := bodyMap["dec"]; !ok { 160 | body.BRC20Decimal = decimal.MAX_PRECISION_STRING 161 | } else { 162 | if v, ok := bodyMap["dec"].(string); ok { 163 | body.BRC20Decimal = v 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | // all ticker (state and history) 171 | type BRC20TokenInfo struct { 172 | UpdateHeight uint32 173 | 174 | Ticker string 175 | Deploy *InscriptionBRC20TickInfo 176 | 177 | History []uint32 178 | HistoryMint []uint32 179 | HistoryInscribeTransfer []uint32 180 | HistoryTransfer []uint32 181 | HistoryWithdraw []uint32 // fixme 182 | } 183 | 184 | type InscriptionBRC20TransferInfo struct { 185 | Tick string 186 | Amount *decimal.Decimal 187 | Data *InscriptionBRC20Data 188 | } 189 | 190 | // inscription info, with mint state 191 | type InscriptionBRC20TickInfo struct { 192 | Data *InscriptionBRC20InfoResp `json:"data"` 193 | Tick string 194 | Amount *decimal.Decimal `json:"-"` 195 | Meta *InscriptionBRC20Data 196 | 197 | SelfMint bool `json:"-"` 198 | 199 | Max *decimal.Decimal `json:"-"` 200 | Limit *decimal.Decimal `json:"-"` 201 | 202 | TotalMinted *decimal.Decimal `json:"-"` 203 | ConfirmedMinted *decimal.Decimal `json:"-"` 204 | ConfirmedMinted1h *decimal.Decimal `json:"-"` 205 | ConfirmedMinted24h *decimal.Decimal `json:"-"` 206 | Burned *decimal.Decimal `json:"-"` 207 | 208 | MintTimes uint32 `json:"-"` 209 | Decimal uint8 `json:"-"` 210 | 211 | TxId string `json:"-"` 212 | Idx uint32 `json:"-"` 213 | Vout uint32 `json:"-"` 214 | Offset uint64 `json:"-"` 215 | 216 | Satoshi uint64 `json:"-"` 217 | PkScript string `json:"-"` 218 | 219 | InscriptionNumber int64 `json:"inscriptionNumber"` 220 | CreateIdxKey string `json:"-"` 221 | Height uint32 `json:"-"` 222 | TxIdx uint32 `json:"-"` 223 | BlockTime uint32 `json:"-"` 224 | 225 | CompleteHeight uint32 `json:"-"` 226 | CompleteBlockTime uint32 `json:"-"` 227 | 228 | InscriptionNumberStart int64 `json:"-"` 229 | InscriptionNumberEnd int64 `json:"-"` 230 | } 231 | 232 | func (d *InscriptionBRC20TickInfo) GetInscriptionId() string { 233 | return fmt.Sprintf("%si%d", utils.HashString([]byte(d.TxId)), d.Idx) 234 | } 235 | 236 | func (in *InscriptionBRC20TickInfo) DeepCopy() (copy *InscriptionBRC20TickInfo) { 237 | copy = &InscriptionBRC20TickInfo{ 238 | Tick: in.Tick, 239 | SelfMint: in.SelfMint, 240 | 241 | Data: in.Data, 242 | Decimal: in.Decimal, 243 | 244 | TxId: in.TxId, 245 | Idx: in.Idx, 246 | Vout: in.Vout, 247 | Offset: in.Offset, 248 | 249 | Satoshi: in.Satoshi, 250 | PkScript: in.PkScript, 251 | 252 | InscriptionNumber: in.InscriptionNumber, 253 | CreateIdxKey: in.CreateIdxKey, 254 | Height: in.Height, 255 | TxIdx: in.TxIdx, 256 | BlockTime: in.BlockTime, 257 | 258 | // runtime value 259 | Max: decimal.NewDecimalCopy(in.Max), 260 | Limit: decimal.NewDecimalCopy(in.Limit), 261 | TotalMinted: decimal.NewDecimalCopy(in.TotalMinted), 262 | ConfirmedMinted: decimal.NewDecimalCopy(in.ConfirmedMinted), 263 | ConfirmedMinted1h: decimal.NewDecimalCopy(in.ConfirmedMinted1h), 264 | ConfirmedMinted24h: decimal.NewDecimalCopy(in.ConfirmedMinted24h), 265 | Burned: decimal.NewDecimalCopy(in.Burned), 266 | Amount: decimal.NewDecimalCopy(in.Amount), 267 | 268 | MintTimes: in.MintTimes, 269 | 270 | CompleteHeight: in.CompleteHeight, 271 | CompleteBlockTime: in.CompleteBlockTime, 272 | 273 | InscriptionNumberStart: in.InscriptionNumberStart, 274 | InscriptionNumberEnd: in.InscriptionNumberEnd, 275 | } 276 | return copy 277 | } 278 | 279 | func NewInscriptionBRC20TickInfo(tick, operation string, data *InscriptionBRC20Data) *InscriptionBRC20TickInfo { 280 | info := &InscriptionBRC20TickInfo{ 281 | Tick: tick, 282 | Data: &InscriptionBRC20InfoResp{ 283 | BRC20Tick: tick, 284 | Operation: operation, 285 | }, 286 | Decimal: 18, 287 | 288 | TxId: data.TxId, 289 | Idx: data.Idx, 290 | Vout: data.Vout, 291 | Offset: data.Offset, 292 | 293 | Satoshi: data.Satoshi, 294 | PkScript: data.PkScript, 295 | 296 | InscriptionNumber: data.InscriptionNumber, 297 | CreateIdxKey: data.CreateIdxKey, 298 | Height: data.Height, 299 | TxIdx: data.TxIdx, 300 | BlockTime: data.BlockTime, 301 | } 302 | return info 303 | } 304 | 305 | // all history for user 306 | type BRC20UserHistory struct { 307 | History []uint32 308 | } 309 | 310 | // state of address for each tick, (balance and history) 311 | type BRC20TokenBalance struct { 312 | UpdateHeight uint32 313 | 314 | Ticker string 315 | PkScript string 316 | AvailableBalance *decimal.Decimal 317 | AvailableBalanceSafe *decimal.Decimal 318 | TransferableBalance *decimal.Decimal 319 | ValidTransferMap map[string]*InscriptionBRC20TickInfo 320 | 321 | History []uint32 322 | HistoryMint []uint32 323 | HistoryInscribeTransfer []uint32 324 | HistorySend []uint32 325 | HistoryReceive []uint32 326 | } 327 | 328 | func (bal *BRC20TokenBalance) OverallBalance() *decimal.Decimal { 329 | return bal.AvailableBalance.Add(bal.TransferableBalance) 330 | } 331 | 332 | func (in *BRC20TokenBalance) DeepCopy() (tb *BRC20TokenBalance) { 333 | tb = &BRC20TokenBalance{ 334 | Ticker: in.Ticker, 335 | PkScript: in.PkScript, 336 | AvailableBalanceSafe: decimal.NewDecimalCopy(in.AvailableBalanceSafe), 337 | AvailableBalance: decimal.NewDecimalCopy(in.AvailableBalance), 338 | TransferableBalance: decimal.NewDecimalCopy(in.TransferableBalance), 339 | } 340 | 341 | tb.ValidTransferMap = make(map[string]*InscriptionBRC20TickInfo, len(in.ValidTransferMap)) 342 | for k, v := range in.ValidTransferMap { 343 | tb.ValidTransferMap[k] = v.DeepCopy() 344 | } 345 | 346 | tb.History = make([]uint32, len(in.History)) 347 | copy(tb.History, in.History) 348 | 349 | tb.HistoryMint = make([]uint32, len(in.HistoryMint)) 350 | copy(tb.HistoryMint, in.HistoryMint) 351 | 352 | tb.HistoryInscribeTransfer = make([]uint32, len(in.HistoryInscribeTransfer)) 353 | copy(tb.HistoryInscribeTransfer, in.HistoryInscribeTransfer) 354 | 355 | tb.HistorySend = make([]uint32, len(in.HistorySend)) 356 | copy(tb.HistorySend, in.HistorySend) 357 | 358 | tb.HistoryReceive = make([]uint32, len(in.HistoryReceive)) 359 | copy(tb.HistoryReceive, in.HistoryReceive) 360 | return tb 361 | } 362 | 363 | // history inscription info 364 | type InscriptionBRC20TickInfoResp struct { 365 | Height uint32 `json:"-"` 366 | Data *InscriptionBRC20InfoResp `json:"data"` 367 | InscriptionNumber int64 `json:"inscriptionNumber"` 368 | InscriptionId string `json:"inscriptionId"` 369 | Satoshi uint64 `json:"satoshi"` 370 | Confirmations int `json:"confirmations"` 371 | } 372 | -------------------------------------------------------------------------------- /model/module.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // decode data 4 | type InscriptionBRC20ModuleDeployContent struct { 5 | Proto string `json:"p,omitempty"` 6 | Operation string `json:"op,omitempty"` 7 | BRC20Name string `json:"name,omitempty"` 8 | BRC20Source string `json:"source,omitempty"` 9 | BRC20Init map[string]interface{} `json:"init,omitempty"` 10 | } 11 | 12 | type InscriptionBRC20ModuleWithdrawContent struct { 13 | Proto string `json:"p,omitempty"` 14 | Operation string `json:"op,omitempty"` 15 | Module string `json:"module,omitempty"` 16 | Tick string `json:"tick,omitempty"` 17 | Amount string `json:"amt,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /model/store.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/unisat-wallet/libbrc20-indexer/decimal" 4 | 5 | // module state store 6 | type BRC20ModuleSwapInfoStore struct { 7 | ID string // module id 8 | Name string // module name 9 | DeployerPkScript string // deployer 10 | SequencerPkScript string // operator, sequencer 11 | GasToPkScript string // 12 | LpFeePkScript string // 13 | 14 | FeeRateSwap string 15 | GasTick string 16 | 17 | History []*BRC20ModuleHistory // history for deploy, deposit, commit, quit 18 | 19 | // runtime for commit 20 | CommitInvalidMap map[string]struct{} // All invalid create commits 21 | CommitIdMap map[string]struct{} // All valid create commits 22 | CommitIdChainMap map[string]struct{} // All connected commits cannot be used as parents for subsequent commits again. 23 | 24 | // token holders in module 25 | // ticker of users in module [address][tick]balanceData 26 | UsersTokenBalanceDataMap map[string]map[string]*BRC20ModuleTokenBalance 27 | 28 | // swap 29 | // lp token balance of address in module [pool][address]balance 30 | LPTokenUsersBalanceMap map[string]map[string]*decimal.Decimal 31 | 32 | // swap total balance 33 | // total balance of pool in module [pool]balanceData 34 | SwapPoolTotalBalanceDataMap map[string]*BRC20ModulePoolTotalBalance 35 | 36 | // module deposit/withdraw state [tick]balanceData 37 | ConditionalApproveStateBalanceDataMap map[string]*BRC20ModuleConditionalApproveStateBalance 38 | } 39 | -------------------------------------------------------------------------------- /utils/bip322/verify.go: -------------------------------------------------------------------------------- 1 | package bip322 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | 7 | "github.com/btcsuite/btcd/btcutil" 8 | "github.com/btcsuite/btcd/chaincfg/chainhash" 9 | "github.com/btcsuite/btcd/txscript" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/unisat-wallet/libbrc20-indexer/utils" 12 | ) 13 | 14 | func GetSha256(data []byte) (hash []byte) { 15 | sha := sha256.New() 16 | sha.Write(data[:]) 17 | hash = sha.Sum(nil) 18 | return 19 | } 20 | 21 | func GetTagSha256(data []byte) (hash []byte) { 22 | tag := []byte("BIP0322-signed-message") 23 | hashTag := GetSha256(tag) 24 | var msg []byte 25 | msg = append(msg, hashTag...) 26 | msg = append(msg, hashTag...) 27 | msg = append(msg, data...) 28 | return GetSha256(msg) 29 | } 30 | 31 | func PrepareTx(pkScript []byte, message string) (toSign *wire.MsgTx, err error) { 32 | // Create a new transaction to spend 33 | toSpend := wire.NewMsgTx(0) 34 | 35 | // Decode the message hash 36 | messageHash := GetTagSha256([]byte(message)) 37 | 38 | // Create the script for to_spend 39 | builder := txscript.NewScriptBuilder() 40 | builder.AddOp(txscript.OP_0) 41 | builder.AddData(messageHash) 42 | scriptSig, err := builder.Script() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // Create a TxIn with the outpoint 000...000:FFFFFFFF 48 | prevOutHash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000") 49 | prevOut := wire.NewOutPoint(prevOutHash, wire.MaxPrevOutIndex) 50 | txIn := wire.NewTxIn(prevOut, scriptSig, nil) 51 | txIn.Sequence = 0 52 | 53 | toSpend.AddTxIn(txIn) 54 | toSpend.AddTxOut(wire.NewTxOut(0, pkScript)) 55 | 56 | // Create a transaction for to_sign 57 | toSign = wire.NewMsgTx(0) 58 | hash := toSpend.TxHash() 59 | 60 | prevOutSpend := wire.NewOutPoint((*chainhash.Hash)(hash.CloneBytes()), 0) 61 | 62 | txSignIn := wire.NewTxIn(prevOutSpend, nil, nil) 63 | txSignIn.Sequence = 0 64 | toSign.AddTxIn(txSignIn) 65 | 66 | // Create the script for to_sign 67 | builderPk := txscript.NewScriptBuilder() 68 | builderPk.AddOp(txscript.OP_RETURN) 69 | scriptPk, err := builderPk.Script() 70 | if err != nil { 71 | return nil, err 72 | } 73 | toSign.AddTxOut(wire.NewTxOut(0, scriptPk)) 74 | return toSign, nil 75 | } 76 | 77 | // VerifySignature 78 | // signature: 64B, pkScript: 33B, message: any 79 | func VerifySignature(witness wire.TxWitness, pkScript []byte, message string) bool { 80 | toSign, err := PrepareTx(pkScript, message) 81 | if err != nil { 82 | fmt.Println("verifying signature, PrepareTx failed:", err) 83 | return false 84 | } 85 | 86 | toSign.TxIn[0].Witness = witness 87 | prevFetcher := txscript.NewCannedPrevOutputFetcher( 88 | pkScript, 0, 89 | ) 90 | hashCache := txscript.NewTxSigHashes(toSign, prevFetcher) 91 | vm, err := txscript.NewEngine(pkScript, toSign, 0, txscript.StandardVerifyFlags, nil, hashCache, 0, prevFetcher) 92 | if err != nil { 93 | return false 94 | } 95 | if err := vm.Execute(); err != nil { 96 | return false 97 | } 98 | return true 99 | } 100 | 101 | func SignSignatureTaproot(pkey, message string) (witness wire.TxWitness, pkScript []byte, err error) { 102 | decodedWif, err := btcutil.DecodeWIF(pkey) 103 | if err != nil { 104 | return nil, nil, err 105 | } 106 | 107 | privKey := decodedWif.PrivKey 108 | pubKey := txscript.ComputeTaprootKeyNoScript(privKey.PubKey()) 109 | pkScript, err = utils.PayToTaprootScript(pubKey) 110 | 111 | toSign, err := PrepareTx(pkScript, message) 112 | if err != nil { 113 | return nil, nil, err 114 | } 115 | 116 | prevFetcher := txscript.NewCannedPrevOutputFetcher( 117 | pkScript, 0, 118 | ) 119 | sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher) 120 | 121 | witness, err = txscript.TaprootWitnessSignature( 122 | toSign, sigHashes, 0, 0, pkScript, 123 | txscript.SigHashDefault, privKey, 124 | ) 125 | return witness, pkScript, nil 126 | } 127 | 128 | func SignSignatureP2WPKH(pkey, message string) (witness wire.TxWitness, pkScript []byte, err error) { 129 | decodedWif, err := btcutil.DecodeWIF(pkey) 130 | if err != nil { 131 | return nil, nil, err 132 | } 133 | 134 | privKey := decodedWif.PrivKey 135 | pubKey := privKey.PubKey() 136 | pkScript, err = utils.PayToWitnessScript(pubKey) 137 | 138 | toSign, err := PrepareTx(pkScript, message) 139 | if err != nil { 140 | return nil, nil, err 141 | } 142 | 143 | prevFetcher := txscript.NewCannedPrevOutputFetcher( 144 | pkScript, 0, 145 | ) 146 | sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher) 147 | 148 | witness, err = txscript.WitnessSignature(toSign, sigHashes, 149 | 0, 0, pkScript, txscript.SigHashAll, 150 | privKey, true) 151 | if err != nil { 152 | return nil, nil, err 153 | } 154 | return witness, pkScript, nil 155 | } 156 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/btcsuite/btcd/btcec/v2" 12 | "github.com/btcsuite/btcd/btcec/v2/schnorr" 13 | "github.com/btcsuite/btcd/btcutil" 14 | "github.com/btcsuite/btcd/chaincfg" 15 | "github.com/btcsuite/btcd/txscript" 16 | ) 17 | 18 | func DecodeTokensFromSwapPair(tickPair string) (token0, token1 string, err error) { 19 | if len(tickPair) != 9 || tickPair[4] != '/' { 20 | return "", "", errors.New("func: removeLiq tickPair invalid") 21 | } 22 | token0 = tickPair[:4] 23 | token1 = tickPair[5:] 24 | 25 | return token0, token1, nil 26 | } 27 | 28 | func GetValidUniqueLowerTickerTicker(ticker string) (lowerTicker string, err error) { 29 | if len(ticker) != 4 && len(ticker) != 5 { 30 | return "", errors.New("ticker len invalid") 31 | } 32 | 33 | lowerTicker = strings.ToLower(ticker) 34 | return lowerTicker, nil 35 | } 36 | 37 | // single sha256 hash 38 | func GetSha256(data []byte) (hash []byte) { 39 | sha := sha256.New() 40 | sha.Write(data[:]) 41 | hash = sha.Sum(nil) 42 | return 43 | } 44 | 45 | func GetHash256(data []byte) (hash []byte) { 46 | sha := sha256.New() 47 | sha.Write(data[:]) 48 | tmp := sha.Sum(nil) 49 | sha.Reset() 50 | sha.Write(tmp) 51 | hash = sha.Sum(nil) 52 | return 53 | } 54 | 55 | func HashString(data []byte) (res string) { 56 | if len(data) != 32 { 57 | return "0000000000000000000000000000000000000000000000000000000000000000" 58 | } 59 | length := 32 60 | var reverseData [32]byte 61 | 62 | // need reverse 63 | for i := 0; i < length; i++ { 64 | reverseData[i] = data[length-i-1] 65 | } 66 | return hex.EncodeToString(reverseData[:]) 67 | } 68 | 69 | func ReverseBytes(data []byte) (result []byte) { 70 | for _, b := range data { 71 | result = append([]byte{b}, result...) 72 | } 73 | return result 74 | } 75 | 76 | // PayToTaprootScript creates a pk script for a pay-to-taproot output key. 77 | func PayToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) { 78 | return txscript.NewScriptBuilder(). 79 | AddOp(txscript.OP_1). 80 | AddData(schnorr.SerializePubKey(taprootKey)). 81 | Script() 82 | } 83 | 84 | // PayToWitnessScript creates a pk script for a pay-to-wpkh output key. 85 | func PayToWitnessScript(pubkey *btcec.PublicKey) ([]byte, error) { 86 | return txscript.NewScriptBuilder(). 87 | AddOp(txscript.OP_0). 88 | AddData(btcutil.Hash160(pubkey.SerializeCompressed())). 89 | Script() 90 | } 91 | 92 | func GetPkScriptByAddress(addr string, netParams *chaincfg.Params) (pk []byte, err error) { 93 | if len(addr) == 0 { 94 | return nil, errors.New("decoded address empty") 95 | } 96 | 97 | addressObj, err := btcutil.DecodeAddress(addr, netParams) 98 | if err != nil { 99 | if len(addr) != 68 || !strings.HasPrefix(addr, "6a20") { 100 | return nil, errors.New("decoded address is of unknown format") 101 | } 102 | // check full hex 103 | pkHex, err := hex.DecodeString(addr) 104 | if err != nil { 105 | return nil, errors.New("decoded address is of unknown format") 106 | } 107 | return pkHex, nil 108 | } 109 | addressPkScript, err := txscript.PayToAddrScript(addressObj) 110 | if err != nil { 111 | return nil, errors.New("decoded address is of unknown format") 112 | } 113 | return addressPkScript, nil 114 | } 115 | 116 | // GetAddressFromScript Use btcsuite to get address 117 | func GetAddressFromScript(script []byte, params *chaincfg.Params) (string, error) { 118 | scriptClass, addresses, _, err := txscript.ExtractPkScriptAddrs(script, params) 119 | if err != nil { 120 | return "", fmt.Errorf("failed to get address: %v", err) 121 | } 122 | 123 | if len(addresses) == 0 { 124 | return "", fmt.Errorf("noaddress") 125 | } 126 | 127 | if scriptClass == txscript.NonStandardTy { 128 | return "", fmt.Errorf("non-standard") 129 | } 130 | 131 | return addresses[0].EncodeAddress(), nil 132 | } 133 | 134 | func GetModuleFromScript(script []byte) (module string, ok bool) { 135 | if len(script) < 34 || len(script) > 38 { 136 | return "", false 137 | } 138 | if script[0] != 0x6a { 139 | return "", false 140 | } 141 | if int(script[1])+2 != len(script) { 142 | return "", false 143 | } 144 | 145 | var idx uint32 146 | if script[1] <= 32 { 147 | idx = uint32(0) 148 | } else if script[1] <= 33 { 149 | idx = uint32(script[34]) 150 | } else if script[1] <= 34 { 151 | idx = uint32(binary.LittleEndian.Uint16(script[34:36])) 152 | } else if script[1] <= 35 { 153 | idx = uint32(script[34]) | uint32(script[35])<<8 | uint32(script[36])<<16 154 | } else if script[1] <= 36 { 155 | idx = binary.LittleEndian.Uint32(script[34:38]) 156 | } 157 | 158 | module = fmt.Sprintf("%si%d", HashString(script[2:34]), idx) 159 | return module, true 160 | } 161 | 162 | func DecodeInscriptionFromBin(script []byte) (id string) { 163 | n := len(script) 164 | if n < 32 || n > 36 { 165 | return "" 166 | } 167 | 168 | var idx uint32 169 | if n == 32 { 170 | idx = uint32(0) 171 | } else if n <= 33 { 172 | idx = uint32(script[32]) 173 | } else if n <= 34 { 174 | idx = uint32(binary.LittleEndian.Uint16(script[32:34])) 175 | } else if n <= 35 { 176 | idx = uint32(script[32]) | uint32(script[33])<<8 | uint32(script[34])<<16 177 | } else if n <= 36 { 178 | idx = binary.LittleEndian.Uint32(script[32:36]) 179 | } 180 | 181 | id = fmt.Sprintf("%si%d", HashString(script[:32]), idx) 182 | return id 183 | } 184 | --------------------------------------------------------------------------------