diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 34871e75..6d1cdcb6 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -20,6 +20,7 @@ import ( "blockbook/bchain/coins/monacoin" "blockbook/bchain/coins/myriad" "blockbook/bchain/coins/namecoin" + "blockbook/bchain/coins/nuls" "blockbook/bchain/coins/pivx" "blockbook/bchain/coins/qtum" "blockbook/bchain/coins/vertcoin" @@ -79,6 +80,7 @@ func init() { BlockChainFactories["Bellcoin"] = bellcoin.NewBellcoinRPC BlockChainFactories["Qtum"] = qtum.NewQtumRPC BlockChainFactories["Qtum Testnet"] = qtum.NewQtumRPC + BlockChainFactories["NULS"] = nuls.NewNulsRPC } // GetCoinNameFromConfig gets coin name and coin shortcut from config file diff --git a/bchain/coins/nuls/nulsparser.go b/bchain/coins/nuls/nulsparser.go new file mode 100644 index 00000000..0b6275c1 --- /dev/null +++ b/bchain/coins/nuls/nulsparser.go @@ -0,0 +1,151 @@ +package nuls + +import ( + "blockbook/bchain" + "blockbook/bchain/coins/btc" + "bytes" + "encoding/binary" + "encoding/json" + "github.com/bsm/go-vlq" + "github.com/martinboehm/btcutil/base58" + + "github.com/martinboehm/btcd/wire" + "github.com/martinboehm/btcutil/chaincfg" +) + +const ( + MainnetMagic wire.BitcoinNet = 0xbd6b0cbf + TestnetMagic wire.BitcoinNet = 0xffcae2ce + RegtestMagic wire.BitcoinNet = 0xdcb7c1fc +) + +var ( + MainNetParams chaincfg.Params + TestNetParams chaincfg.Params + RegtestParams chaincfg.Params +) + +func init() { + MainNetParams = chaincfg.MainNetParams + MainNetParams.Net = MainnetMagic + + // Address encoding magics + MainNetParams.PubKeyHashAddrID = []byte{38} // base58 prefix: G + MainNetParams.ScriptHashAddrID = []byte{10} // base58 prefix: W + + TestNetParams = chaincfg.TestNet3Params + TestNetParams.Net = TestnetMagic + + // Address encoding magics + TestNetParams.PubKeyHashAddrID = []byte{140} // base58 prefix: y + TestNetParams.ScriptHashAddrID = []byte{19} // base58 prefix: 8 or 9 + + RegtestParams = chaincfg.RegressionNetParams + RegtestParams.Net = RegtestMagic + + // Address encoding magics + RegtestParams.PubKeyHashAddrID = []byte{140} // base58 prefix: y + RegtestParams.ScriptHashAddrID = []byte{19} // base58 prefix: 8 or 9 +} + +// NulsParser handle +type NulsParser struct { + *btc.BitcoinParser +} + +// NewNulsParser returns new NulsParser instance +func NewNulsParser(params *chaincfg.Params, c *btc.Configuration) *NulsParser { + return &NulsParser{BitcoinParser: btc.NewBitcoinParser(params, c)} +} + +// GetChainParams contains network parameters for the main Gincoin network, +// the regression test Gincoin network, the test Gincoin network and +// the simulation test Gincoin network, in this order +func GetChainParams(chain string) *chaincfg.Params { + if !chaincfg.IsRegistered(&MainNetParams) { + err := chaincfg.Register(&MainNetParams) + if err == nil { + err = chaincfg.Register(&TestNetParams) + } + if err == nil { + err = chaincfg.Register(&RegtestParams) + } + if err != nil { + panic(err) + } + } + switch chain { + case "test": + return &TestNetParams + case "regtest": + return &RegtestParams + default: + return &MainNetParams + } +} + +// PackedTxidLen returns length in bytes of packed txid +func (p *NulsParser) PackedTxidLen() int { + return 34 +} + +// GetAddrDescFromAddress returns internal address representation (descriptor) of given address +func (p *NulsParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) { + addressByte := base58.Decode(address) + return bchain.AddressDescriptor(addressByte), nil +} + +// GetAddrDescFromVout returns internal address representation (descriptor) of given transaction output +func (p *NulsParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) { + addressStr := output.ScriptPubKey.Hex + addressByte := base58.Decode(addressStr) + return bchain.AddressDescriptor(addressByte), nil +} + +// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable +func (p *NulsParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) { + var addrs []string + + if addrDesc != nil { + addrs = append(addrs, base58.Encode(addrDesc)) + } + + return addrs, true, nil +} + +// PackTx packs transaction to byte array +func (p *NulsParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { + txBytes, error := json.Marshal(tx) + if error != nil { + return nil, error + } + + buf := make([]byte, 4+vlq.MaxLen64) + binary.BigEndian.PutUint32(buf[0:4], height) + vlq.PutInt(buf[4:4+vlq.MaxLen64], blockTime) + resByes := bytes.Join([][]byte{buf, txBytes}, []byte("")) + return resByes, nil +} + +// UnpackTx unpacks transaction from byte array +func (p *NulsParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { + height := binary.BigEndian.Uint32(buf) + bt, _ := vlq.Int(buf[4 : 4+vlq.MaxLen64]) + tx, err := p.ParseTx(buf[4+vlq.MaxLen64:]) + if err != nil { + return nil, 0, err + } + tx.Blocktime = bt + + return tx, height, nil +} + +func (p *NulsParser) ParseTx(b []byte) (*bchain.Tx, error) { + tx := bchain.Tx{} + err := json.Unmarshal(b, &tx) + + if err != nil { + return nil, err + } + return &tx, err +} diff --git a/bchain/coins/nuls/nulsparser_test.go b/bchain/coins/nuls/nulsparser_test.go new file mode 100644 index 00000000..614000cd --- /dev/null +++ b/bchain/coins/nuls/nulsparser_test.go @@ -0,0 +1,343 @@ +package nuls + +import ( + "blockbook/bchain" + "blockbook/bchain/coins/btc" + "encoding/hex" + "encoding/json" + "math/big" + "reflect" + "testing" +) + +var ( + testTx1, testTx2 bchain.Tx + + testTxPacked1 = "0001e240daadfbe7931e000000007b22686578223a22222c2274786964223a223030323036626231323431303861356664393865383238623138316666303162393237633063366234373764343531326266656638346366353266306663326136613161222c2276657273696f6e223a312c226c6f636b74696d65223a302c2276696e223a5b7b22636f696e62617365223a22222c2274786964223a223030323035343537616230373033623164313034363264343334373034386330626233353634653430663537616531663632353136393539343364653161633831306130222c22766f7574223a302c22736372697074536967223a7b22686578223a22227d2c2273657175656e6365223a302c22616464726573736573223a5b224e736535334d77524c424a31575555365365644d485141466643507442377734225d7d5d2c22766f7574223a5b7b2256616c7565536174223a3339393939393030303030302c2276616c7565223a302c226e223a302c227363726970745075624b6579223a7b22686578223a224e7365347a705a4873557555376835796d7632387063476277486a75336a6f56222c22616464726573736573223a5b224e7365347a705a4873557555376835796d7632387063476277486a75336a6f56225d7d7d5d2c22626c6f636b74696d65223a313535323335373834343137357d" + testTxPacked2 = "0007c91adaadfbb89946000000007b22686578223a22222c2274786964223a223030323037386139386633383163373134613036386436303565346265316565323139353438353736313165303938616262636333663530633536383066386164326535222c2276657273696f6e223a312c226c6f636b74696d65223a302c2276696e223a5b7b22636f696e62617365223a22222c2274786964223a223030323037613430646334623661633430376434396133633333616137353462623466303565343565353763323438313162313437653762663363616630363361383233222c22766f7574223a312c22736372697074536967223a7b22686578223a22227d2c2273657175656e6365223a302c22616464726573736573223a5b224e73653131397a326f53444a596b466b786d775944695974506642654e6b7169225d7d5d2c22766f7574223a5b7b2256616c7565536174223a3430303030303030303030302c2276616c7565223a302c226e223a302c227363726970745075624b6579223a7b22686578223a224e736534696b6a45383867324267734e7773737754646b53776953724b6a6a53222c22616464726573736573223a5b224e736534696b6a45383867324267734e7773737754646b53776953724b6a6a53225d7d7d2c7b2256616c7565536174223a373238363536353537303030302c2276616c7565223a302c226e223a312c227363726970745075624b6579223a7b22686578223a224e73653131397a326f53444a596b466b786d775944695974506642654e6b7169222c22616464726573736573223a5b224e73653131397a326f53444a596b466b786d775944695974506642654e6b7169225d7d7d5d2c22626c6f636b74696d65223a313535323335373435393535357d" +) + +func init() { + testTx1 = bchain.Tx{ + Hex: "", + Blocktime: 1552357844175, + Txid: "00206bb124108a5fd98e828b181ff01b927c0c6b477d4512bfef84cf52f0fc2a6a1a", + LockTime: 0, + Version: 1, + Vin: []bchain.Vin{ + { + Txid: "00205457ab0703b1d10462d4347048c0bb3564e40f57ae1f6251695943de1ac810a0", + Vout: 0, + Sequence: 0, + Addresses: []string{ + "Nse53MwRLBJ1WUU6SedMHQAFfCPtB7w4", + }, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(399999000000), + N: 0, + JsonValue: json.Number("0"), + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "Nse4zpZHsUuU7h5ymv28pcGbwHju3joV", + Addresses: []string{ + "Nse4zpZHsUuU7h5ymv28pcGbwHju3joV", + }, + }, + }, + }, + //CoinSpecificData: []string{}, + } + + testTx2 = bchain.Tx{ + Hex: "", + Blocktime: 1552357459555, + Txid: "002078a98f381c714a068d605e4be1ee21954857611e098abbcc3f50c5680f8ad2e5", + LockTime: 0, + Version: 1, + Vin: []bchain.Vin{ + { + Txid: "00207a40dc4b6ac407d49a3c33aa754bb4f05e45e57c24811b147e7bf3caf063a823", + Vout: 1, + Sequence: 0, + Addresses: []string{ + "Nse119z2oSDJYkFkxmwYDiYtPfBeNkqi", + }, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(400000000000), + N: 0, + JsonValue: json.Number("0"), + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "Nse4ikjE88g2BgsNwsswTdkSwiSrKjjS", + Addresses: []string{ + "Nse4ikjE88g2BgsNwsswTdkSwiSrKjjS", + }, + }, + }, + { + ValueSat: *big.NewInt(7286565570000), + N: 1, + JsonValue: json.Number("0"), + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "Nse119z2oSDJYkFkxmwYDiYtPfBeNkqi", + Addresses: []string{ + "Nse119z2oSDJYkFkxmwYDiYtPfBeNkqi", + }, + }, + }, + }, + //CoinSpecificData: []string{}, + } +} + +func TestGetAddrDescFromAddress(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "P2PKH", + args: args{address: "Nse4j39uEMuxx5j577h3K2MDLAQ64JZN"}, + want: "042301ac78cd3eb193287e2c59e4cbd765c5c47d432c2fa1", + wantErr: false, + }, + { + name: "P2PKH", + args: args{address: "Nse2e7U7nmGT8UHsvQ7JfksLtWwoLwrd"}, + want: "0423018a90e66a64318f6af6d673487a6560f5686fd26a2e", + wantErr: false, + }, + { + name: "P2PKH", + args: args{address: "NsdvMEP57nzxmBa5z18rx9sQsLgUfNtw"}, + want: "04230124422cfe426573e476fd45d7c2a43a75a0b6b8c478", + wantErr: false, + }, + } + parser := NewNulsParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.GetAddrDescFromAddress(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("GetAddrDescFromAddress() = %v, want %v", h, tt.want) + } + }) + } +} + +func TestGetAddrDescFromVout(t *testing.T) { + type args struct { + vout bchain.Vout + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "P2PK", + args: args{vout: bchain.Vout{ScriptPubKey: bchain.ScriptPubKey{Hex: "Nse4j39uEMuxx5j577h3K2MDLAQ64JZN"}}}, + want: "042301ac78cd3eb193287e2c59e4cbd765c5c47d432c2fa1", + wantErr: false, + }, + { + name: "P2PK", + args: args{vout: bchain.Vout{ScriptPubKey: bchain.ScriptPubKey{Hex: "Nse2e7U7nmGT8UHsvQ7JfksLtWwoLwrd"}}}, + want: "0423018a90e66a64318f6af6d673487a6560f5686fd26a2e", + wantErr: false, + }, + { + name: "P2PK", + args: args{vout: bchain.Vout{ScriptPubKey: bchain.ScriptPubKey{Hex: "NsdvMEP57nzxmBa5z18rx9sQsLgUfNtw"}}}, + want: "04230124422cfe426573e476fd45d7c2a43a75a0b6b8c478", + wantErr: false, + }, + } + parser := NewNulsParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.GetAddrDescFromVout(&tt.args.vout) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddrDescFromVout() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("GetAddrDescFromVout() = %v, want %v", h, tt.want) + } + }) + } +} + +func TestGetAddressesFromAddrDesc(t *testing.T) { + type args struct { + script string + } + tests := []struct { + name string + args args + want []string + want2 bool + wantErr bool + }{ + { + name: "P2PKH", + args: args{script: "042301ac78cd3eb193287e2c59e4cbd765c5c47d432c2fa1"}, + want: []string{"Nse4j39uEMuxx5j577h3K2MDLAQ64JZN"}, + want2: true, + wantErr: false, + }, + { + name: "P2PKH", + args: args{script: "0423018a90e66a64318f6af6d673487a6560f5686fd26a2e"}, + want: []string{"Nse2e7U7nmGT8UHsvQ7JfksLtWwoLwrd"}, + want2: true, + wantErr: false, + }, + { + name: "P2PKH", + args: args{script: "04230124422cfe426573e476fd45d7c2a43a75a0b6b8c478"}, + want: []string{"NsdvMEP57nzxmBa5z18rx9sQsLgUfNtw"}, + want2: true, + wantErr: false, + }, + } + + parser := NewNulsParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.script) + got, got2, err := parser.GetAddressesFromAddrDesc(b) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddressesFromAddrDesc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got2, tt.want2) + } + }) + } +} + +func TestPackTx(t *testing.T) { + type args struct { + tx bchain.Tx + height uint32 + blockTime int64 + parser *NulsParser + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "test-1", + args: args{ + tx: testTx1, + height: 123456, + blockTime: 1552357844175, + parser: NewNulsParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: testTxPacked1, + wantErr: false, + }, + { + name: "test-2", + args: args{ + tx: testTx2, + height: 510234, + blockTime: 1552357459555, + parser: NewNulsParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: testTxPacked2, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.PackTx(&tt.args.tx, tt.args.height, tt.args.blockTime) + if (err != nil) != tt.wantErr { + t.Errorf("packTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("packTx() = %v, want %v", h, tt.want) + } + }) + } +} + +func TestUnpackTx(t *testing.T) { + type args struct { + packedTx string + parser *NulsParser + } + tests := []struct { + name string + args args + want *bchain.Tx + want1 uint32 + wantErr bool + }{ + { + name: "test-1", + args: args{ + packedTx: testTxPacked1, + parser: NewNulsParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: &testTx1, + want1: 123456, + wantErr: false, + }, + { + name: "test-2", + args: args{ + packedTx: testTxPacked2, + parser: NewNulsParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: &testTx2, + want1: 510234, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.packedTx) + got, got1, err := tt.args.parser.UnpackTx(b) + if (err != nil) != tt.wantErr { + t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("unpackTx() got = %+v, want %+v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("unpackTx() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/bchain/coins/nuls/nulsrpc.go b/bchain/coins/nuls/nulsrpc.go new file mode 100644 index 00000000..3bd7b24e --- /dev/null +++ b/bchain/coins/nuls/nulsrpc.go @@ -0,0 +1,635 @@ +package nuls + +import ( + "blockbook/bchain" + "blockbook/bchain/coins/btc" + "bytes" + "encoding/base64" + "encoding/json" + "github.com/gobuffalo/packr/v2/file/resolver/encoding/hex" + "github.com/juju/errors" + "io" + "io/ioutil" + "math/big" + "net" + "net/http" + "runtime/debug" + "strconv" + "time" + + "github.com/golang/glog" +) + +// NulsRPC is an interface to JSON-RPC bitcoind service. +type NulsRPC struct { + *btc.BitcoinRPC + client http.Client + rpcURL string + user string + password string +} + +// NewNulsRPC returns new NulsRPC instance. +func NewNulsRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + b, err := btc.NewBitcoinRPC(config, pushHandler) + if err != nil { + return nil, err + } + + var c btc.Configuration + err = json.Unmarshal(config, &c) + if err != nil { + return nil, errors.Annotatef(err, "Invalid configuration file") + } + + transport := &http.Transport{ + Dial: (&net.Dialer{KeepAlive: 600 * time.Second}).Dial, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, // necessary to not to deplete ports + } + + s := &NulsRPC{ + BitcoinRPC: b.(*btc.BitcoinRPC), + client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport}, + rpcURL: c.RPCURL, + user: c.RPCUser, + password: c.RPCPass, + } + s.BitcoinRPC.RPCMarshaler = btc.JSONMarshalerV1{} + s.BitcoinRPC.ChainConfig.SupportsEstimateSmartFee = false + + return s, nil +} + +// Initialize initializes GincoinRPC instance. +func (n *NulsRPC) Initialize() error { + chainName := "" + params := GetChainParams(chainName) + + // always create parser + n.BitcoinRPC.Parser = NewNulsParser(params, n.BitcoinRPC.ChainConfig) + + // parameters for getInfo request + if params.Net == MainnetMagic { + n.BitcoinRPC.Testnet = false + n.BitcoinRPC.Network = "livenet" + } else { + n.BitcoinRPC.Testnet = true + n.BitcoinRPC.Network = "testnet" + } + + glog.Info("rpc: block chain ", params.Name) + + return nil +} + +type CmdGetNetworkInfo struct { + Success bool `json:"success"` + Data struct { + LocalBestHeight int64 `json:"localBestHeight"` + NetBestHeight int `json:"netBestHeight"` + TimeOffset string `json:"timeOffset"` + InCount int8 `json:"inCount"` + OutCount int8 `json:"outCount"` + } `json:"data"` +} + +type CmdGetVersionInfo struct { + Success bool `json:"success"` + Data struct { + MyVersion string `json:"myVersion"` + NewestVersion string `json:"newestVersion"` + NetworkVersion int `json:"networkVersion"` + Infromation string `json:"infromation"` + } `json:"data"` +} + +type CmdGetBestBlockHash struct { + Success bool `json:"success"` + Data struct { + Value string `json:"value"` + } `json:"data"` +} + +type CmdGetBestBlockHeight struct { + Success bool `json:"success"` + Data struct { + Value uint32 `json:"value"` + } `json:"data"` +} + +type CmdTxBroadcast struct { + Success bool `json:"success"` + Data struct { + Value string `json:"value"` + } `json:"data"` +} + +type CmdGetBlockHeader struct { + Success bool `json:"success"` + Data struct { + Hash string `json:"hash"` + PreHash string `json:"preHash"` + MerkleHash string `json:"merkleHash"` + StateRoot string `json:"stateRoot"` + Time int64 `json:"time"` + Height int64 `json:"height"` + TxCount int `json:"txCount"` + PackingAddress string `json:"packingAddress"` + ConfirmCount int `json:"confirmCount"` + ScriptSig string `json:"scriptSig"` + Size int `json:"size"` + Reward float64 `json:"reward"` + Fee float64 `json:"fee"` + } `json:"data"` +} + +type CmdGetBlock struct { + Success bool `json:"success"` + Data struct { + Hash string `json:"hash"` + PreHash string `json:"preHash"` + MerkleHash string `json:"merkleHash"` + StateRoot string `json:"stateRoot"` + Time int64 `json:"time"` + Height int64 `json:"height"` + TxCount int `json:"txCount"` + PackingAddress string `json:"packingAddress"` + ConfirmCount int `json:"confirmCount"` + ScriptSig string `json:"scriptSig"` + Size int `json:"size"` + Reward float64 `json:"reward"` + Fee float64 `json:"fee"` + + TxList []Tx `json:"txList"` + } `json:"data"` +} + +type CmdGetTx struct { + Success bool `json:"success"` + Tx Tx `json:"data"` +} + +type Tx struct { + Hash string `json:"hash"` + Type int `json:"type"` + Time int64 `json:"time"` + BlockHeight int64 `json:"blockHeight"` + Fee float64 `json:"fee"` + Value float64 `json:"value"` + Remark string `json:"remark"` + ScriptSig string `json:"scriptSig"` + Status int `json:"status"` + ConfirmCount int `json:"confirmCount"` + Size int `json:"size"` + Inputs []struct { + FromHash string `json:"fromHash"` + FromIndex uint32 `json:"fromIndex"` + Address string `json:"address"` + Value float64 `json:"value"` + } `json:"inputs"` + Outputs []struct { + Address string `json:"address"` + Value int64 `json:"value"` + LockTime int64 `json:"lockTime"` + } `json:"outputs"` +} + +type CmdGetTxBytes struct { + Success bool `json:"success"` + Data struct { + Value string `json:"value"` + } `json:"data"` +} + +func (n *NulsRPC) GetChainInfo() (*bchain.ChainInfo, error) { + networkInfo := CmdGetNetworkInfo{} + error := n.Call("/api/network/info", &networkInfo) + if error != nil { + return nil, error + } + + versionInfo := CmdGetVersionInfo{} + error = n.Call("/api/client/version", &versionInfo) + if error != nil { + return nil, error + } + + chainInfo := &bchain.ChainInfo{ + Chain: "nuls", + Blocks: networkInfo.Data.NetBestHeight, + Headers: networkInfo.Data.NetBestHeight, + Bestblockhash: "", + Difficulty: networkInfo.Data.TimeOffset, + SizeOnDisk: networkInfo.Data.LocalBestHeight, + Version: versionInfo.Data.MyVersion, + Subversion: versionInfo.Data.NewestVersion, + ProtocolVersion: strconv.Itoa(versionInfo.Data.NetworkVersion), + Timeoffset: 0, + Warnings: versionInfo.Data.Infromation, + } + return chainInfo, nil +} + +func (n *NulsRPC) GetBestBlockHash() (string, error) { + bestBlockHash := CmdGetBestBlockHash{} + error := n.Call("/api/block/newest/hash", &bestBlockHash) + if error != nil { + return "", error + } + return bestBlockHash.Data.Value, nil +} + +func (n *NulsRPC) GetBestBlockHeight() (uint32, error) { + bestBlockHeight := CmdGetBestBlockHeight{} + error := n.Call("/api/block/newest/height", &bestBlockHeight) + if error != nil { + return 0, error + } + return bestBlockHeight.Data.Value, nil +} + +func (n *NulsRPC) GetBlockHash(height uint32) (string, error) { + blockHeader := CmdGetBlockHeader{} + error := n.Call("/api/block/header/height/"+strconv.Itoa(int(height)), &blockHeader) + if error != nil { + return "", error + } + + if !blockHeader.Success { + return "", bchain.ErrBlockNotFound + } + + return blockHeader.Data.Hash, nil +} + +func (n *NulsRPC) GetBlockHeader(hash string) (*bchain.BlockHeader, error) { + uri := "/api/block/header/hash/" + hash + return n.getBlobkHeader(uri) +} + +func (n *NulsRPC) GetBlockHeaderByHeight(height uint32) (*bchain.BlockHeader, error) { + uri := "/api/block/header/height/" + strconv.Itoa(int(height)) + return n.getBlobkHeader(uri) +} + +func (n *NulsRPC) getBlobkHeader(uri string) (*bchain.BlockHeader, error) { + blockHeader := CmdGetBlockHeader{} + error := n.Call(uri, &blockHeader) + if error != nil { + return nil, error + } + + if !blockHeader.Success { + return nil, bchain.ErrBlockNotFound + } + + nexHash, _ := n.GetBlockHash(uint32(blockHeader.Data.Height + 1)) + + header := &bchain.BlockHeader{ + Hash: blockHeader.Data.Hash, + Prev: blockHeader.Data.PreHash, + Next: nexHash, + Height: uint32(blockHeader.Data.Height), + Confirmations: blockHeader.Data.ConfirmCount, + Size: blockHeader.Data.Size, + Time: blockHeader.Data.Time / 1000, + } + return header, nil +} + +func (n *NulsRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { + + url := "/api/block/hash/" + hash + + if hash == "" { + url = "/api/block/height/" + strconv.Itoa(int(height)) + } + + getBlock := CmdGetBlock{} + error := n.Call(url, &getBlock) + if error != nil { + return nil, error + } + + if !getBlock.Success { + return nil, bchain.ErrBlockNotFound + } + + nexHash, _ := n.GetBlockHash(uint32(getBlock.Data.Height + 1)) + + header := bchain.BlockHeader{ + Hash: getBlock.Data.Hash, + Prev: getBlock.Data.PreHash, + Next: nexHash, + Height: uint32(getBlock.Data.Height), + Confirmations: getBlock.Data.ConfirmCount, + Size: getBlock.Data.Size, + Time: getBlock.Data.Time / 1000, + } + + var txs []bchain.Tx + + for _, rawTx := range getBlock.Data.TxList { + tx, err := converTx(rawTx) + if err != nil { + return nil, err + } + tx.Blocktime = header.Time + txs = append(txs, *tx) + } + + block := &bchain.Block{ + BlockHeader: header, + Txs: txs, + } + return block, nil +} + +func (n *NulsRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { + if hash == "" { + return nil, bchain.ErrBlockNotFound + } + + getBlock := CmdGetBlock{} + error := n.Call("/api/block/hash/"+hash, &getBlock) + if error != nil { + return nil, error + } + + if !getBlock.Success { + return nil, bchain.ErrBlockNotFound + } + + nexHash, _ := n.GetBlockHash(uint32(getBlock.Data.Height + 1)) + + header := bchain.BlockHeader{ + Hash: getBlock.Data.Hash, + Prev: getBlock.Data.PreHash, + Next: nexHash, + Height: uint32(getBlock.Data.Height), + Confirmations: getBlock.Data.ConfirmCount, + Size: getBlock.Data.Size, + Time: getBlock.Data.Time / 1000, + } + + var txIds []string + + for _, rawTx := range getBlock.Data.TxList { + txIds = append(txIds, rawTx.Hash) + } + + blockInfo := &bchain.BlockInfo{ + BlockHeader: header, + MerkleRoot: getBlock.Data.MerkleHash, + //Version: getBlock.Data.StateRoot, + Txids: txIds, + } + return blockInfo, nil +} + +func (n *NulsRPC) GetMempool() ([]string, error) { + return nil, nil +} + +func (n *NulsRPC) GetTransaction(txid string) (*bchain.Tx, error) { + if txid == "" { + return nil, bchain.ErrTxidMissing + } + + getTx := CmdGetTx{} + error := n.Call("/api/tx/hash/"+txid, &getTx) + if error != nil { + return nil, error + } + + if !getTx.Success { + return nil, bchain.ErrTxNotFound + } + + tx, err := converTx(getTx.Tx) + if err != nil { + return nil, err + } + + blockHeaderHeight := getTx.Tx.BlockHeight + blockHeader, e := n.GetBlockHeaderByHeight(uint32(blockHeaderHeight)) + if blockHeader != nil { + tx.Blocktime = blockHeader.Time + } + + hexBytys, e := n.GetTransactionSpecific(tx) + if e == nil { + var hex string + json.Unmarshal(hexBytys, &hex) + tx.Hex = hex + tx.CoinSpecificData = hex + } + return tx, nil +} + +func (n *NulsRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) { + return nil, nil +} + +func (n *NulsRPC) GetTransactionSpecific(tx *bchain.Tx) (json.RawMessage, error) { + if tx == nil { + return nil, bchain.ErrTxNotFound + } + + if csd, ok := tx.CoinSpecificData.(json.RawMessage); ok { + return csd, nil + } + + getTxBytes := CmdGetTxBytes{} + error := n.Call("/api/tx/bytes?hash="+tx.Txid, &getTxBytes) + if error != nil { + return nil, error + } + + if !getTxBytes.Success { + return nil, bchain.ErrTxNotFound + } + + txBytes, byErr := base64.StdEncoding.DecodeString(getTxBytes.Data.Value) + if byErr != nil { + return nil, byErr + } + hexBytes := make([]byte, len(txBytes)*2) + hex.Encode(hexBytes, txBytes) + + m, err := json.Marshal(string(hexBytes)) + return json.RawMessage(m), err +} + +func (n *NulsRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { + return n.EstimateFee(blocks) +} + +func (n *NulsRPC) EstimateFee(blocks int) (big.Int, error) { + return *big.NewInt(100000), nil +} + +func (n *NulsRPC) SendRawTransaction(tx string) (string, error) { + broadcast := CmdTxBroadcast{} + req := struct { + TxHex string `json:"txHex"` + }{ + TxHex: tx, + } + + error := n.Post("/api/accountledger/transaction/broadcast", req, &broadcast) + if error != nil { + return "", error + } + + if !broadcast.Success { + return "", bchain.ErrTxidMissing + } + + return broadcast.Data.Value, nil +} + +func (n *NulsRPC) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) ([]bchain.Outpoint, error) { + return nil, nil +} + +func (n *NulsRPC) ResyncMempool(onNewTxAddr bchain.OnNewTxAddrFunc) (int, error) { + return 0, nil +} + +// Call calls Backend RPC interface, using RPCMarshaler interface to marshall the request +func (b *NulsRPC) Call(uri string, res interface{}) error { + url := b.rpcURL + uri + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + httpReq.SetBasicAuth(b.user, b.password) + httpRes, err := b.client.Do(httpReq) + // in some cases the httpRes can contain data even if it returns error + // see http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/ + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + // if server returns HTTP error code it might not return json with response + // handle both cases + if httpRes.StatusCode != 200 { + err = safeDecodeResponse(httpRes.Body, &res) + if err != nil { + return errors.Errorf("%v %v", httpRes.Status, err) + } + return nil + } + return safeDecodeResponse(httpRes.Body, &res) +} + +func (b *NulsRPC) Post(uri string, req interface{}, res interface{}) error { + url := b.rpcURL + uri + httpData, err := b.RPCMarshaler.Marshal(req) + if err != nil { + return err + } + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(httpData)) + if err != nil { + return err + } + httpReq.SetBasicAuth(b.user, b.password) + httpReq.Header.Set("Content-Type", "application/json") + httpRes, err := b.client.Do(httpReq) + // in some cases the httpRes can contain data even if it returns error + // see http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/ + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + // if server returns HTTP error code it might not return json with response + // handle both cases + if httpRes.StatusCode != 200 { + err = safeDecodeResponse(httpRes.Body, &res) + if err != nil { + return errors.Errorf("%v %v", httpRes.Status, err) + } + return nil + } + return safeDecodeResponse(httpRes.Body, &res) +} + +func safeDecodeResponse(body io.ReadCloser, res *interface{}) (err error) { + var data []byte + defer func() { + if r := recover(); r != nil { + glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data)) + debug.PrintStack() + if len(data) > 0 && len(data) < 2048 { + err = errors.Errorf("Error: %v", string(data)) + } else { + err = errors.New("Internal error") + } + } + }() + data, err = ioutil.ReadAll(body) + if err != nil { + return err + } + + //fmt.Println(string(data)) + error := json.Unmarshal(data, res) + return error +} + +func converTx(rawTx Tx) (*bchain.Tx, error) { + var lockTime int64 = 0 + var vins = make([]bchain.Vin, 0) + var vouts []bchain.Vout + + for _, input := range rawTx.Inputs { + vin := bchain.Vin{ + Coinbase: "", + Txid: input.FromHash, + Vout: input.FromIndex, + ScriptSig: bchain.ScriptSig{}, + Sequence: 0, + Addresses: []string{input.Address}, + } + vins = append(vins, vin) + } + + for index, output := range rawTx.Outputs { + vout := bchain.Vout{ + ValueSat: *big.NewInt(output.Value), + //JsonValue: "", + //LockTime: output.LockTime, + N: uint32(index), + ScriptPubKey: bchain.ScriptPubKey{ + Hex: output.Address, + Addresses: []string{ + output.Address, + }, + }, + } + vouts = append(vouts, vout) + + if lockTime < output.LockTime { + lockTime = output.LockTime + } + } + + tx := &bchain.Tx{ + Hex: "", + Txid: rawTx.Hash, + Version: 0, + LockTime: uint32(lockTime), + Vin: vins, + Vout: vouts, + Confirmations: uint32(rawTx.ConfirmCount), + Time: rawTx.Time / 1000, + } + + return tx, nil +} diff --git a/configs/coins/nuls.json b/configs/coins/nuls.json new file mode 100644 index 00000000..29ce6308 --- /dev/null +++ b/configs/coins/nuls.json @@ -0,0 +1,67 @@ +{ + "coin": { + "name": "NULS", + "shortcut": "NULS", + "label": "NULS", + "alias": "nuls" + }, + "ports": { + "backend_rpc": 8053, + "backend_message_queue": 38353, + "blockbook_internal": 9053, + "blockbook_public": 9153 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-nuls", + "package_revision": "1.0-1", + "system_user": "nuls", + "version": "1.2.1", + "binary_url": "http://47.52.78.92/nuls-wallet/NULS-Wallet-linux64-1.2.1.tar.gz", + "verification_type": "sha256", + "verification_source": "8732e94415e9b6d51e9c7e1921002b4e43d78e2b12f2de636af53a562b4f4381", + "extract_command": "tar -C backend --strip 0 -xf", + "exclude_files": [], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/nuls.sh --port {{.Ports.BackendRPC}} --dataDir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --pid-file /run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": false, + "mainnet": true, + "server_config_file": "bitcoin_like.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "addnode": [ + "mainnet.nuls" + ] + } + }, + "blockbook": { + "package_name": "blockbook-nuls", + "system_user": "blockbook-nuls", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-resyncindexperiod=11000", + "block_chain": { + "parse": true, + "mempool_workers": 4, + "mempool_sub_workers": 8, + "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 133, + "additional_params": {} + } + }, + "meta": { + "package_maintainer": "NULS Core Team", + "package_maintainer_email": "ln@nuls.io" + } +} \ No newline at end of file diff --git a/tests/rpc/testdata/nuls.json b/tests/rpc/testdata/nuls.json new file mode 100644 index 00000000..4ecf423f --- /dev/null +++ b/tests/rpc/testdata/nuls.json @@ -0,0 +1,174 @@ +{ + "blockHeight": 2092530, + "blockHash": "002038a2301c19dec173b6776369f4985a66a4c583c7fa26ae9db7a4369643e71370", + "blockTime": 1552392940, + "blockSize": 290, + "blockTxs": [ + "0020d682b3832334ed03177a7c51a0d94c6cdaee313d14e147203d55ed0f66202234", + "0020ad9d437d9867c778c39a04c030fe941c7c07d9529cf534935471c7c8703ee7c7" + ], + "txDetails": { + "0020d682b3832334ed03177a7c51a0d94c6cdaee313d14e147203d55ed0f66202234": { + "hex": "0100e069d471690100ffffffff000c170423015b98f40fb24b34d395a774aa5ea939c89999a7679e0bff0d00000000daf11f00000017042301a1bdaf958e76c84ff8a17189a6f1f4269ad4a54b7a050f0000000000daf11f00000017042301e60bf29198ed95d53013fdae781f1272a6ccdbda90d70e0000000000daf11f00000017042301751c960ae6c2f9052a53878c9e15f08ad4e79774accb130000000000daf11f00000017042301f69e4b0348458a4846f06dc4940fc1393edd3fcfea7e0e0000000000daf11f000000170423012cfbf9441677fcea3af313da99c01b485e117e9d4f5e0c0000000000daf11f00000017042301d6d33afa523fc78011aafc0c60e2e8dc45206e5676610c0000000000daf11f00000017042301e9e760b295568f955526b29e0d3a0c1dd3a1c50a4f5e0c0000000000daf11f000000170423017e8d9ff74a624f5ee2ef1a9d665385b7314c73a1750c0d0000000000daf11f000000170423012a3435e83852742f65574f297be94c48d9ab68fbb535350100000000daf11f000000170423017904ac680eb1b6a37652fe7fbc57d545d1447d48e55f0c0000000000daf11f00000017042301d2b1c26cdcb86896890bbb741e32d8057ad78cff9dbc180000000000daf11f00000000", + "txid": "0020d682b3832334ed03177a7c51a0d94c6cdaee313d14e147203d55ed0f66202234", + "blocktime": 1552392940, + "time": 1552392940, + "locktime": 2093530, + "version": 0, + "vin": [], + "vout": [ + { + "value": 2.34818462, + "n": 0, + "scriptPubKey": { + "hex": "NsdykbfjmZVHYNaVrKVF89UzUgPKaRa9", + "Addresses": ["NsdykbfjmZVHYNaVrKVF89UzUgPKaRa9"] + } + }, + { + "value": 0.00984442, + "n": 1, + "scriptPubKey": { + "hex": "Nse44mP6EJWbv9qPEPgi4sjtx8QmLzjU", + "Addresses": ["Nse44mP6EJWbv9qPEPgi4sjtx8QmLzjU"] + } + }, + { + "value": 0.00972688, + "n": 2, + "scriptPubKey": { + "hex": "Nse8GNwGtoBhxGmwfetqnhnMART4MVGU", + "Addresses": ["Nse8GNwGtoBhxGmwfetqnhnMART4MVGU"] + } + }, + { + "value": 0.01297324, + "n": 3, + "scriptPubKey": { + "hex": "Nse1KbUNjc8mEGAHgCRHXRWYR6zFa9dh", + "Addresses": ["Nse1KbUNjc8mEGAHgCRHXRWYR6zFa9dh"] + } + }, + { + "value": 0.00949994, + "n": 4, + "scriptPubKey": { + "hex": "Nse9HUun2HwFAypdpBkbjXbQPMkM7nQD", + "Addresses": ["Nse9HUun2HwFAypdpBkbjXbQPMkM7nQD"] + } + }, + { + "value": 0.00810575, + "n": 5, + "scriptPubKey": { + "hex": "NsdvtMNdncJC3q8un2DpBWacZL6YRzJp", + "Addresses": ["NsdvtMNdncJC3q8un2DpBWacZL6YRzJp"] + } + }, + { + "value": 0.00811382, + "n": 6, + "scriptPubKey": { + "hex": "Nse7L6F3sEGc5NRghLtkRrVGaxwgXePS", + "Addresses": ["Nse7L6F3sEGc5NRghLtkRrVGaxwgXePS"] + } + }, + { + "value": 0.00810575, + "n": 7, + "scriptPubKey": { + "hex": "Nse8W8pMG1E94HQSkieUYUyASBr6oRQr", + "Addresses": ["Nse8W8pMG1E94HQSkieUYUyASBr6oRQr"] + } + }, + { + "value": 0.00855157, + "n": 8, + "scriptPubKey": { + "hex": "Nse1uGXjbCm9JJjmL88xYhnU59B6b3GR", + "Addresses": ["Nse1uGXjbCm9JJjmL88xYhnU59B6b3GR"] + } + }, + { + "value": 0.20264373, + "n": 9, + "scriptPubKey": { + "hex": "NsdviSF29wA91UtGgYuN3HL6iMWLs2hc", + "Addresses": ["NsdviSF29wA91UtGgYuN3HL6iMWLs2hc"] + } + }, + { + "value": 0.00810981, + "n": 10, + "scriptPubKey": { + "hex": "Nse1ZXaeJDcuteKwbxacdBnX46bzfcuQ", + "Addresses": ["Nse1ZXaeJDcuteKwbxacdBnX46bzfcuQ"] + } + }, + { + "value": 0.01621149, + "n": 11, + "scriptPubKey": { + "hex": "Nse75MmS9stKYE11KCaLaVRaXHLprikS", + "Addresses": ["Nse75MmS9stKYE11KCaLaVRaXHLprikS"] + } + } + ] + }, + "0020ad9d437d9867c778c39a04c030fe941c7c07d9529cf534935471c7c8703ee7c7": { + "hex": "02004a53d471690100ffffffff03230020199146789cda517776c140a8638218b8fa29e3a1857573f9463df3e6fa1f56100167537e3b680000000000000000002300208fa6362e79b40b61791dd1484b79d61c9bebf4dc4986f347ba46e65feb9ee4dc00e02c504d00000000000000000000230020fe3ca565e667bd6b2b7889c1307d2006d0ce5697d13e98311c605ca802c7ef0f00603924cb2e0200000000000000000217042301405aa9eeca8af1edae196837bde4eef29bee040600664280d1000000000000000000170423016056a17c02cd53191ce2e647c5ed02d1c70e483607cdaed3c50100000000000000006b2102f23b91b10ca411dceccf873a313399f0c3a90b14e7b0b5f43642e33da882b7bc00473045022100c2521e5951d168e43f42010e8f1ba484cb445dcdb29d066caf0bce1c78476624022074df5ccdd4eda6f3adec5420efef85d7369b9b8266bbc392f5e76eef94f0b9a9", + "txid": "0020ad9d437d9867c778c39a04c030fe941c7c07d9529cf534935471c7c8703ee7c7", + "blocktime": 1552392940, + "time": 1552392934, + "locktime": 0, + "version": 0, + "vin": [ + { + "txid": "0020199146789cda517776c140a8638218b8fa29e3a1857573f9463df3e6fa1f5610", + "vout": 1, + "sequence": 0, + "scriptSig": { + "hex": "" + }, + "Addresses": ["Nsdz3WNA1bfGSmy5oYY3KGMjZSd1C61p"] + }, + { + "txid": "00208fa6362e79b40b61791dd1484b79d61c9bebf4dc4986f347ba46e65feb9ee4dc", + "vout": 0, + "sequence": 0, + "scriptSig": { + "hex": "" + }, + "Addresses": ["Nsdz3WNA1bfGSmy5oYY3KGMjZSd1C61p"] + }, + { + "txid": "0020fe3ca565e667bd6b2b7889c1307d2006d0ce5697d13e98311c605ca802c7ef0f", + "vout": 0, + "sequence": 0, + "scriptSig": { + "hex": "" + }, + "Addresses": ["Nsdz3WNA1bfGSmy5oYY3KGMjZSd1C61p"] + } + ], + "vout": [ + { + "value": 8998.00000000, + "n": 0, + "scriptPubKey": { + "hex": "Nsdx5SBU9xN9SKz6dEwcz85WdFaNj2g4", + "Addresses": ["Nsdx5SBU9xN9SKz6dEwcz85WdFaNj2g4"] + } + }, + { + "value": 19491.71633415, + "n": 1, + "scriptPubKey": { + "hex": "Nsdz3WNA1bfGSmy5oYY3KGMjZSd1C61p", + "Addresses": ["Nsdz3WNA1bfGSmy5oYY3KGMjZSd1C61p"] + } + } + ] + } + } +} diff --git a/tests/sync/testdata/nuls.json b/tests/sync/testdata/nuls.json new file mode 100644 index 00000000..63579dcb --- /dev/null +++ b/tests/sync/testdata/nuls.json @@ -0,0 +1,546 @@ +{ + "connectBlocks": { + "syncRanges": [ + {"lower": 2184003, "upper": 2184213} + ], + "blocks": { + "2184003": { + "height": 2184003, + "hash": "0020e281a2e5095904f95e3d1f3b48da808c7511e478362f22e678bf68b976e1bbb6", + "noTxs": 1, + "txDetails": [ + { + "hex": "", + "txid": "00208f0b652c397e761e0e2e1fc31f8b1b4f7fe28a1f35af382886c4d95c8dcb9715", + "blocktime": 1553317770, + "time": 1553317770, + "version": 0, + "vin": [], + "vout": [ + { + "value": 0.21957008, + "n": 0, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "NsdteeXfqPcsmMStKeE2Es3dv7rSXjgF", + "Addresses": ["NsdteeXfqPcsmMStKeE2Es3dv7rSXjgF"] + } + }, + { + "value": 0.00836727, + "n": 1, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse4H3MD4thD2rZeW178J6AxUh4B5Dq3", + "Addresses": ["Nse4H3MD4thD2rZeW178J6AxUh4B5Dq3"] + } + }, + { + "value": 0.00823145, + "n": 2, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nsdx6yBvJRtguGduzxkgxEPjkMebW9DC", + "Addresses": ["Nsdx6yBvJRtguGduzxkgxEPjkMebW9DC"] + } + }, + { + "value": 0.00992713, + "n": 3, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse72BM97n88MFyhWDp9iDPEbkw4fqVh", + "Addresses": ["Nse72BM97n88MFyhWDp9iDPEbkw4fqVh"] + } + }, + { + "value": 0.41158111, + "n": 4, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse2c2siYena4BvSn82iprugFJE4gUsJ", + "Addresses": ["Nse2c2siYena4BvSn82iprugFJE4gUsJ"] + } + }, + { + "value": 0.00823145, + "n": 5, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "NsdxARm7PHjiPLgnhMMbXLR2n7R2eded", + "Addresses": ["NsdxARm7PHjiPLgnhMMbXLR2n7R2eded"] + } + },{ + "value": 0.01243773, + "n": 6, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "NsdycgzrFdoev6nmxxMGJvXKTYzo36vj", + "Addresses": ["NsdycgzrFdoev6nmxxMGJvXKTYzo36vj"] + } + }, + { + "value": 0.01009588, + "n": 7, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse5p6NULbecLCe8mZVVzBcSMpaZy2bB", + "Addresses": ["Nse5p6NULbecLCe8mZVVzBcSMpaZy2bB"] + } + },{ + "value": 0.00823145, + "n": 8, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse4e7QYA3Mj9UUqcGpCbYKexwgtSCvz", + "Addresses": ["Nse4e7QYA3Mj9UUqcGpCbYKexwgtSCvz"] + } + }, + { + "value": 0.00823145, + "n": 9, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "NsdyL8HuKgrQehmefzZgKo2aydfRJQSU", + "Addresses": ["NsdyL8HuKgrQehmefzZgKo2aydfRJQSU"] + } + }, + { + "value": 0.01646950, + "n": 10, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse8jHzR4cS1iyiJkewGHKEWqm5B7ZbA", + "Addresses": ["Nse8jHzR4cS1iyiJkewGHKEWqm5B7ZbA"] + } + }, + { + "value": 0.00823145, + "n": 11, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "NsdveDWtHKGCbxVH1AKkMA2oB2VUv134", + "Addresses": ["NsdveDWtHKGCbxVH1AKkMA2oB2VUv134"] + } + }, + { + "value": 0.01704543, + "n": 12, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nsdz1QdJb7xNE4bDNg13oXMfa2jtbh36", + "Addresses": ["Nsdz1QdJb7xNE4bDNg13oXMfa2jtbh36"] + } + }, + { + "value": 0.03778238, + "n": 13, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse4rPNt3S3HHZaRhuXRDfVFYSKA4TzR", + "Addresses": ["Nse4rPNt3S3HHZaRhuXRDfVFYSKA4TzR"] + } + }, + { + "value": 0.41155642, + "n": 14, + "lockTime": 2185003, + "scriptPubKey": { + "hex": "Nse84iqmZipKg7Cn6LtV5uQshWwtdRMJ", + "Addresses": ["Nse84iqmZipKg7Cn6LtV5uQshWwtdRMJ"] + } + } + ] + } + ] + }, + "2184004": { + "height": 2184004, + "hash": "002099f701eb56f8ff3486d10a4e981df04791bcc47f6749d10fbec0987bf425e8d7", + "noTxs": 1, + "txDetails": [ + { + "hex": "", + "txid": "0020ecd57d05aa56098114d2284cf48ab6edceab2351aa43add3af09b3a956fb458e", + "blocktime": 1553317780, + "time": 1553317780, + "version": 0, + "vin": [], + "vout": [ + { + "value": 2.39921315, + "n": 0, + "lockTime": 2185004, + "scriptPubKey": { + "hex": "NsdwtSWZrG5gwUDoR7VfxxCVzxYsNm3Y", + "Addresses": ["NsdwtSWZrG5gwUDoR7VfxxCVzxYsNm3Y"] + } + } + ] + } + ] + }, + "2184005": { + "height": 2184005, + "hash": "00203f9e06e59bda071f7af9fef59a53604e59a6640c52635f6e18101396b616de82", + "noTxs": 1, + "txDetails": [ + { + "hex": "", + "txid": "002011a00b4561f8bb278d8e4744550368b217d61013fcd1eebc61f44a1c6a164765", + "blocktime": 1553317790, + "time": 1553317790, + "version": 0, + "vin": [], + "vout": [ + { + "value": 0.57503254, + "n": 0, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nsduq2g457guft4bpj44BVFqVow6kyWi", + "Addresses": ["Nsduq2g457guft4bpj44BVFqVow6kyWi"] + } + }, + { + "value": 0.04152482, + "n": 1, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "NsdvZbRf6pY1ggVfwfBR8DVs4KwUyd7L", + "Addresses": ["NsdvZbRf6pY1ggVfwfBR8DVs4KwUyd7L"] + } + }, + { + "value": 0.02018105, + "n": 2, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse7uEeE7aRrhFyio9ZLa2wpx7aZ6oBf", + "Addresses": ["Nse7uEeE7aRrhFyio9ZLa2wpx7aZ6oBf"] + } + }, + { + "value": 0.01177228, + "n": 3, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse2EYyQXm5EntkGGNU1omz4AZVVXRyP", + "Addresses": ["Nse2EYyQXm5EntkGGNU1omz4AZVVXRyP"] + } + }, + { + "value": 0.02932482, + "n": 4, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "NsdyewTUzSVwyyk55oL4WA7mR3tEt7cb", + "Addresses": ["NsdyewTUzSVwyyk55oL4WA7mR3tEt7cb"] + } + }, + { + "value": 0.08304964, + "n": 5, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "NsdtpVCa8nMtcZoWcvBDDKdmvvvQiqB4", + "Addresses": ["NsdtpVCa8nMtcZoWcvBDDKdmvvvQiqB4"] + } + },{ + "value": 0.16040263, + "n": 6, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "NsdupfMS5mSvxTquqKdAtzzqrbzGrEVX", + "Addresses": ["NsdupfMS5mSvxTquqKdAtzzqrbzGrEVX"] + } + }, + { + "value": 0.02906321, + "n": 7, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nsdya19a18roRTWLKNZ4ivsswRLcQq3m", + "Addresses": ["Nsdya19a18roRTWLKNZ4ivsswRLcQq3m"] + } + },{ + "value": 0.02074533, + "n": 8, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse3mU18MXLYTuGGvCXyk3U1Zmf3d129", + "Addresses": ["Nse3mU18MXLYTuGGvCXyk3U1Zmf3d129"] + } + }, + { + "value": 0.04303632, + "n": 9, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse11hHcyLS9pidxowEB6Nupf6rp1y83", + "Addresses": ["Nse11hHcyLS9pidxowEB6Nupf6rp1y83"] + } + }, + { + "value": 0.24783259, + "n": 10, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse3rftMFxSy3vhRsyyK1axLrD1f4gvF", + "Addresses": ["Nse3rftMFxSy3vhRsyyK1axLrD1f4gvF"] + } + }, + { + "value": 0.03612242, + "n": 11, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse8aukpCpwW43qnogQN3WV2SHjbgUeU", + "Addresses": ["Nse8aukpCpwW43qnogQN3WV2SHjbgUeU"] + } + }, + { + "value": 0.09346822, + "n": 12, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse31sLCY9iDtsU1Fb5nwMzHQx1SC2pW", + "Addresses": ["Nse31sLCY9iDtsU1Fb5nwMzHQx1SC2pW"] + } + }, + { + "value": 0.04232209, + "n": 13, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse3heixrSj8Ad2iYymSqZLEASEx46ct", + "Addresses": ["Nse3heixrSj8Ad2iYymSqZLEASEx46ct"] + } + }, + { + "value": 0.22471157, + "n": 14, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse16kQr7z2msh5gerMbPDCNyZJFFgui", + "Addresses": ["Nse16kQr7z2msh5gerMbPDCNyZJFFgui"] + } + }, + { + "value": 0.02124410, + "n": 15, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "NsdwqY7WsekKmzmwdbgkack2VZMo1ehq", + "Addresses": ["NsdwqY7WsekKmzmwdbgkack2VZMo1ehq"] + } + }, + { + "value": 0.04152482, + "n": 16, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nsdzsz17JDYQbEmZReG8vqveoXhRXVqL", + "Addresses": ["Nsdzsz17JDYQbEmZReG8vqveoXhRXVqL"] + } + }, + { + "value": 0.20762411, + "n": 17, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse2rtdaQLBPiyoVmJH4BQHJn2VUSHjo", + "Addresses": ["Nse2rtdaQLBPiyoVmJH4BQHJn2VUSHjo"] + } + }, + { + "value": 0.09136708, + "n": 18, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse7W2dvVKFT2UYoavU4vVFeSYG9rjb1", + "Addresses": ["Nse7W2dvVKFT2UYoavU4vVFeSYG9rjb1"] + } + }, + { + "value": 0.03410019, + "n": 19, + "lockTime": 2185005, + "scriptPubKey": { + "hex": "Nse1PbFcq9TJirXsvsjMooZ5RM1A8y7C", + "Addresses": ["Nse1PbFcq9TJirXsvsjMooZ5RM1A8y7C"] + } + } + ] + } + ] + }, + "2184031": { + "height": 2184031, + "hash": "00202ef14e5a5505e8b8658af6c15110c1b9cf4f5dead3cfb363bbaa5ffb2275ee6e", + "noTxs": 2, + "txDetails": [ + { + "hex": "", + "txid": "00206d1d22475202bbb81faba6b0e9ba22fe35c7e9d1e542d3284c5002c5a2ef7ca5", + "blocktime": 1553318050, + "time": 1553318050, + "version": 0, + "vin": [], + "vout": [ + { + "value": 1.14055774, + "n": 0, + "lockTime": 2185031, + "scriptPubKey": { + "hex": "Nse5cCyitYFJ7j6ncn5UCst7pc672QEL", + "Addresses": ["Nse5cCyitYFJ7j6ncn5UCst7pc672QEL"] + } + }, + { + "value": 0.02054451, + "n": 1, + "lockTime": 2185031, + "scriptPubKey": { + "hex": "Nse24Dta8UYJtR6jZHSqWtrfdRpnfHYd", + "Addresses": ["Nse24Dta8UYJtR6jZHSqWtrfdRpnfHYd"] + } + }, + { + "value": 0.35934295, + "n": 2, + "lockTime": 2185031, + "scriptPubKey": { + "hex": "Nse5S92swYm231iDVZNTrDurMcGCWBcM", + "Addresses": ["Nse5S92swYm231iDVZNTrDurMcGCWBcM"] + } + }, + { + "value": 0.38161210, + "n": 3, + "lockTime": 2185031, + "scriptPubKey": { + "hex": "NsdviSF29wA91UtGgYuN3HL6iMWLs2hc", + "Addresses": ["NsdviSF29wA91UtGgYuN3HL6iMWLs2hc"] + } + }, + { + "value": 0.32403928, + "n": 4, + "lockTime": 2185031, + "scriptPubKey": { + "hex": "Nse8nchECsiWQ2eyTgzjvH4DewAWqvpR", + "Addresses": ["Nse8nchECsiWQ2eyTgzjvH4DewAWqvpR"] + } + } + ] + }, + { + "hex": "", + "txid": "0020643416a8b1dad09ee2c1bb2d12aa88eef77a6462a6bd16c39fd3971d67d66919", + "blocktime": 1553318050, + "time": 1553318045, + "version": 0, + "vin": [ + { + "txid": "00205462276c5fb51b465c3926dceb27a6a724e63410c146bb027fdd7f7f06bcb56f", + "vout": 0, + "sequence": 0, + "scriptSig": { + "hex": "" + } + } + ], + "vout": [ + { + "value": 5444.999, + "n": 0, + "lockTime": 0, + "scriptPubKey": { + "hex": "Nsdwnd4auFisFJKU6iDvBxTdPkeg8qkB", + "Addresses": ["Nsdwnd4auFisFJKU6iDvBxTdPkeg8qkB"] + } + } + ] + } + ] + }, + "2184213": { + "height": 2184213, + "hash": "002052a452263860be5bba31a3aace06c1cb524d48cda3d04575c3881b10241cca06", + "noTxs": 1, + "txDetails": [ + { + "hex": "", + "txid": "00204882a15d826d4401aa765510243f455f7f6a05b0e77a9edd420d366cabd98818", + "blocktime": 1553319870, + "time": 1553319870, + "version": 0, + "vin": [], + "vout": [ + { + "value": 0.04070938, + "n": 0, + "lockTime": 2185213, + "scriptPubKey": { + "hex": "NsdwTB38bAwybyKTfUcxZoLGEHSVqpNE", + "Addresses": ["NsdwTB38bAwybyKTfUcxZoLGEHSVqpNE"] + } + }, + { + "value": 0.20863453, + "n": 1, + "lockTime": 2185213, + "scriptPubKey": { + "hex": "Nse8Q7gk2gVGDdHk7rxasedRRjgVc18Q", + "Addresses": ["Nse8Q7gk2gVGDdHk7rxasedRRjgVc18Q"] + } + } + ] + } + ] + } + } + }, + "handleFork": { + "syncRanges": [ + {"lower": 2184210, "upper": 2184213} + ], + "fakeBlocks": { + "2184210": { + "height": 2184210, + "hash": "00205087cb3e6964a51b2be17593660ed4fa460706742d0d97092541d604b7ef4eaa" + }, + "2184211": { + "height": 2184211, + "hash": "00204ba087c1ab04a45cfef6c61c16f97b72d118dc08fc627d7216b4a38257e906c4" + }, + "2184212": { + "height": 2184212, + "hash": "002029540932d67260a4dbfb7796ffe2a96bf3abbd5c4898814b8a3057c5392d0cbb" + }, + "2184213": { + "height": 2184213, + "hash": "00209d3d7e5869f1f32d4bd75587691f426d21e28022e98bd7ef0ca76590c8a6eb76" + } + }, + "realBlocks": { + "2184210": { + "height": 2184210, + "hash": "00200fc994cceb3aa75a9adbdbfa5d2b9dbfaddb8ca88f1cd90acc43e0e775abfff3" + }, + "2184211": { + "height": 2184211, + "hash": "0020a3d90cf5962177a775f47d2edaed94b2519d0cde93711d5e95878c9c6d390b85" + }, + "2184212": { + "height": 2184212, + "hash": "002027d151738f460437db0ce8aaa058cd5fa9230a4e59e5257cc53e059f05419c3c" + }, + "2184213": { + "height": 2184213, + "hash": "002052a452263860be5bba31a3aace06c1cb524d48cda3d04575c3881b10241cca06" + } + } + } +} diff --git a/tests/tests.json b/tests/tests.json index df34f0b4..82a45ccb 100644 --- a/tests/tests.json +++ b/tests/tests.json @@ -125,5 +125,9 @@ "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateSmartFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] + }, + "nuls": { + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], + "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] } }