diff --git a/neoutils/mobile.go b/neoutils/mobile.go index e890a8d..63c127e 100644 --- a/neoutils/mobile.go +++ b/neoutils/mobile.go @@ -2,21 +2,30 @@ package neoutils import ( "fmt" + "strconv" + "strings" - "github.com/o3labs/neo-utils/neoutils/coz" + "github.com/o3labs/neo-utils/neoutils/o3" "github.com/o3labs/neo-utils/neoutils/smartcontract" ) // This class contains simplified method designed specifically for gomobile bind // gomobile bind doesn't support slice argument or return -func utxoFromNEONWalletDB(neonWalletDBEndpoint string, address string) (smartcontract.Unspent, error) { - //"http://localhost:5000/" - cozClient := coz.NewClient(neonWalletDBEndpoint) +func utxoFromO3Platform(network string, address string) (smartcontract.Unspent, error) { - unspentCoz, err := cozClient.GetUnspentByAddress(address) - if err != nil { - return smartcontract.Unspent{}, err + unspent := smartcontract.Unspent{ + Assets: map[smartcontract.NativeAsset]*smartcontract.Balance{}, + } + + client := o3.DefaultO3APIClient() + if network == "test" { + client = o3.APIClientWithNEOTestnet() + } + + response := client.GetNEOUTXO(address) + if response.Code != 200 { + return unspent, fmt.Errorf("Error cannot get utxo") } gasBalance := smartcontract.Balance{ @@ -29,26 +38,32 @@ func utxoFromNEONWalletDB(neonWalletDBEndpoint string, address string) (smartcon UTXOs: []smartcontract.UTXO{}, } - for _, v := range unspentCoz.GAS.Unspent { - gasTX1 := smartcontract.UTXO{ - Index: v.Index, - TXID: v.Txid, - Value: v.Value, + for _, v := range response.Result.Data { + if strings.Contains(v.Asset, string(smartcontract.GAS)) { + value, err := strconv.ParseFloat(v.Value, 64) + if err != nil { + continue + } + gasTX1 := smartcontract.UTXO{ + Index: v.Index, + TXID: v.Txid, + Value: value, + } + gasBalance.UTXOs = append(gasBalance.UTXOs, gasTX1) } - gasBalance.UTXOs = append(gasBalance.UTXOs, gasTX1) - } - for _, v := range unspentCoz.NEO.Unspent { - tx := smartcontract.UTXO{ - Index: v.Index, - TXID: v.Txid, - Value: v.Value, + if strings.Contains(v.Asset, string(smartcontract.NEO)) { + value, err := strconv.ParseFloat(v.Value, 64) + if err != nil { + continue + } + tx := smartcontract.UTXO{ + Index: v.Index, + TXID: v.Txid, + Value: value, + } + neoBalance.UTXOs = append(neoBalance.UTXOs, tx) } - neoBalance.UTXOs = append(neoBalance.UTXOs, tx) - } - - unspent := smartcontract.Unspent{ - Assets: map[smartcontract.NativeAsset]*smartcontract.Balance{}, } unspent.Assets[smartcontract.GAS] = &gasBalance @@ -61,7 +76,7 @@ type RawTransaction struct { Data []byte } -func MintTokensRawTransactionMobile(utxoEndpoint string, scriptHash string, wif string, sendingAssetID string, amount float64, remark string, networkFeeAmountInGAS float64) (*RawTransaction, error) { +func MintTokensRawTransactionMobile(network string, scriptHash string, wif string, sendingAssetID string, amount float64, remark string, networkFeeAmountInGAS float64) (*RawTransaction, error) { rawTransaction := &RawTransaction{} fee := smartcontract.NetworkFeeAmount(networkFeeAmountInGAS) nep5 := UseNEP5WithNetworkFee(scriptHash, fee) @@ -70,7 +85,7 @@ func MintTokensRawTransactionMobile(utxoEndpoint string, scriptHash string, wif return nil, err } - unspent, err := utxoFromNEONWalletDB(utxoEndpoint, wallet.Address) + unspent, err := utxoFromO3Platform(network, wallet.Address) if err != nil { return nil, err } diff --git a/neoutils/mobile_test.go b/neoutils/mobile_test.go index e2046e2..82a0ab5 100644 --- a/neoutils/mobile_test.go +++ b/neoutils/mobile_test.go @@ -26,11 +26,10 @@ func TestMintTokensFromMobile(t *testing.T) { // gas := string(smartcontract.GAS) amount := float64(2) remark := "o3x" - // utxoEndpoint := "http://localhost:5000/" - utxoEndpoint := "http://testnet-api.wallet.cityofzion.io/" + network := "test" networkFeeAmountInGAS := float64(0.0011) - tx, err := neoutils.MintTokensRawTransactionMobile(utxoEndpoint, scriptHash, wif, neo, amount, remark, networkFeeAmountInGAS) + tx, err := neoutils.MintTokensRawTransactionMobile(network, scriptHash, wif, neo, amount, remark, networkFeeAmountInGAS) if err != nil { log.Printf("%v", err) t.Fail() diff --git a/neoutils/nep5.go b/neoutils/nep5.go index 56b62cb..4661202 100644 --- a/neoutils/nep5.go +++ b/neoutils/nep5.go @@ -2,7 +2,6 @@ package neoutils import ( "fmt" - "log" "strings" "github.com/o3labs/neo-utils/neoutils/smartcontract" @@ -114,7 +113,6 @@ func (n *NEP5) TransferNEP5RawTransaction(wallet Wallet, toAddress smartcontract func (n *NEP5) MintTokensRawTransaction(wallet Wallet, assetToSend smartcontract.NativeAsset, amount float64, unspent smartcontract.Unspent, remark string) ([]byte, string, error) { needVerification := true - log.Printf("needVerification = %v", needVerification) operation := "mintTokens" args := []interface{}{} attributes := map[smartcontract.TransactionAttribute][]byte{} diff --git a/neoutils/network.go b/neoutils/network.go index 071637e..3c0798f 100644 --- a/neoutils/network.go +++ b/neoutils/network.go @@ -6,6 +6,7 @@ import ( "net/http" "sort" "strings" + "sync" "time" ) @@ -112,6 +113,7 @@ func SelectBestSeedNode(commaSeparatedURLs string) *SeedNodeResponse { urls := strings.Split(commaSeparatedURLs, ",") ch := make(chan *FetchSeedRequest, len(urls)) fetchedList := []string{} + wg := sync.WaitGroup{} listHighestNodes := []SeedNodeResponse{} for _, url := range urls { go func(url string) { @@ -119,7 +121,9 @@ func SelectBestSeedNode(commaSeparatedURLs string) *SeedNodeResponse { ch <- &FetchSeedRequest{res, url} }(url) } + wg.Add(1) +loop: for { select { case request := <-ch: @@ -134,21 +138,24 @@ func SelectBestSeedNode(commaSeparatedURLs string) *SeedNodeResponse { fetchedList = append(fetchedList, request.URL) if len(fetchedList) == len(urls) { - if len(listHighestNodes) == 0 { - return nil - } - //using sort.SliceStable to sort min response time first - sort.SliceStable(listHighestNodes, func(i, j int) bool { - return listHighestNodes[i].ResponseTime < listHighestNodes[j].ResponseTime - }) - //using sort.SliceStable to sort block count and preserve the sorted position - sort.SliceStable(listHighestNodes, func(i, j int) bool { - return listHighestNodes[i].BlockCount > listHighestNodes[j].BlockCount - }) - - return &listHighestNodes[0] + // if len(listHighestNodes) == 0 { + // continue + // } + wg.Done() + break loop } } } - return nil + //wait for the operation + wg.Wait() + //using sort.SliceStable to sort min response time first + sort.SliceStable(listHighestNodes, func(i, j int) bool { + return listHighestNodes[i].ResponseTime < listHighestNodes[j].ResponseTime + }) + //using sort.SliceStable to sort block count and preserve the sorted position + sort.SliceStable(listHighestNodes, func(i, j int) bool { + return listHighestNodes[i].BlockCount > listHighestNodes[j].BlockCount + }) + + return &listHighestNodes[0] } diff --git a/neoutils/network_test.go b/neoutils/network_test.go index ae34ae2..53005a0 100644 --- a/neoutils/network_test.go +++ b/neoutils/network_test.go @@ -38,5 +38,17 @@ func TestBestNode(t *testing.T) { if best != nil { log.Printf("best node %+v %v %vms", best.URL, best.BlockCount, best.ResponseTime) } +} +func TestGetBestO3Node(t *testing.T) { + urls := []string{ + "http://seed1.o3node.org:10332", + "http://seed2.o3node.org:10332", + "http://seed3.o3node.org:10332", + } + commaSeparated := strings.Join(urls, ",") + best := SelectBestSeedNode(commaSeparated) + if best != nil { + log.Printf("best node %+v %v %vms", best.URL, best.BlockCount, best.ResponseTime) + } } diff --git a/neoutils/o3/api.go b/neoutils/o3/api.go new file mode 100644 index 0000000..8ef8a3d --- /dev/null +++ b/neoutils/o3/api.go @@ -0,0 +1,95 @@ +package o3 + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" +) + +const apiEndpoint = "https://platform.o3.network/api" + +type NEONetWork string + +var NEOMainNet = "main" +var NEOTestNet = "test" + +type O3APIInterface interface { + GetNEOUTXO(address string) UTXOResponse + GetNEOClimableGAS(address string) ClaimableGASResponse +} + +type O3Client struct { + APIBaseEndpoint url.URL + neoNetwork string +} + +func DefaultO3APIClient() *O3Client { + u, err := url.Parse(apiEndpoint) + if err != nil { + return nil + } + return &O3Client{APIBaseEndpoint: *u} +} + +func APIClientWithNEOTestnet() *O3Client { + u, err := url.Parse(apiEndpoint) + if err != nil { + return nil + } + return &O3Client{APIBaseEndpoint: *u, neoNetwork: "test"} +} + +//make sure all method interface is implemented +var _ O3APIInterface = (*O3Client)(nil) + +func (n *O3Client) makeGETRequest(endpoint string, out interface{}) error { + + fullEndpointString := fmt.Sprintf("%v%v", n.APIBaseEndpoint.String(), endpoint) + fullEndpoint, _ := url.Parse(fullEndpointString) + + if n.neoNetwork == "test" { + log.Printf("network = test") + q := fullEndpoint.Query() + q.Set("network", n.neoNetwork) + fullEndpoint.RawQuery = q.Encode() + } + + log.Printf("%v", fullEndpoint.String()) + + req, err := http.NewRequest("GET", fullEndpoint.String(), nil) + if err != nil { + return err + } + req.Header.Add("content-type", "application/json") + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + err = json.NewDecoder(res.Body).Decode(&out) + if err != nil { + return err + } + + return nil +} + +func (o *O3Client) GetNEOUTXO(address string) UTXOResponse { + response := UTXOResponse{} + err := o.makeGETRequest(fmt.Sprintf("/v1/neo/%v/utxo", address), &response) + if err != nil { + return response + } + return response +} +func (o *O3Client) GetNEOClimableGAS(address string) ClaimableGASResponse { + response := ClaimableGASResponse{} + + err := o.makeGETRequest(fmt.Sprintf("/v1/neo/%v/claimablegas", address), &response) + if err != nil { + return response + } + return response +} diff --git a/neoutils/o3/api_test.go b/neoutils/o3/api_test.go new file mode 100644 index 0000000..850e661 --- /dev/null +++ b/neoutils/o3/api_test.go @@ -0,0 +1,38 @@ +package o3_test + +import ( + "log" + "testing" + + "github.com/o3labs/neo-utils/neoutils/o3" +) + +func TestO3NEOUTXO(t *testing.T) { + client := o3.DefaultO3APIClient() + response := client.GetNEOUTXO("ANk325vGG5kcc6Dcnk6zkoEBHY4E6es2nY") + if response.Code != 200 { + t.Fail() + } + log.Printf("%+v", response) +} + +func TestO3NEOUTXOTestnet(t *testing.T) { + client := o3.APIClientWithNEOTestnet() + response := client.GetNEOUTXO("ANk325vGG5kcc6Dcnk6zkoEBHY4E6es2nY") + if response.Code != 200 { + t.Fail() + } + log.Printf("%+v", response) +} + +func TestO3GetClaimableGAS(t *testing.T) { + client := o3.DefaultO3APIClient() + response := client.GetNEOClimableGAS("ANk325vGG5kcc6Dcnk6zkoEBHY4E6es2nY") + if response.Code != 200 { + t.Fail() + } + log.Printf("%+v", response.Result.Data.Gas) + for _, v := range response.Result.Data.Claims { + log.Printf("%+v", v) + } +} diff --git a/neoutils/o3/response.go b/neoutils/o3/response.go new file mode 100644 index 0000000..2d47006 --- /dev/null +++ b/neoutils/o3/response.go @@ -0,0 +1,45 @@ +package o3 + +type Response struct { + Code int `json:"code"` +} + +type ErrorResponse struct { + Error struct { + Message string `json:"message"` + Type string `json:"type"` + } `json:"error"` +} + +type UTXOResponse struct { + Response + *ErrorResponse + Result struct { + Data []UTXOResultData `json:"data"` + } `json:"result"` +} + +type UTXOResultData struct { + Asset string `json:"asset"` + Index int `json:"index"` + Txid string `json:"txid"` + Value string `json:"value"` + CreatedAtBlock int `json:"createdAtBlock"` +} + +type ClaimableGASResponse struct { + Response + *ErrorResponse + Result struct { + Data struct { + Gas string `json:"gas"` + Claims []struct { + Asset string `json:"asset"` + Index int `json:"index"` + Txid string `json:"txid"` + Value string `json:"value"` + CreatedAtBlock int `json:"createdAtBlock"` + } `json:"claims"` + } `json:"data"` + } `json:"result"` +} diff --git a/neoutils/smartcontract/scriptbuilder.go b/neoutils/smartcontract/scriptbuilder.go index e97ed30..97b6165 100644 --- a/neoutils/smartcontract/scriptbuilder.go +++ b/neoutils/smartcontract/scriptbuilder.go @@ -6,7 +6,6 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "log" "github.com/o3labs/neo-utils/neoutils/btckey" "golang.org/x/crypto/ripemd160" @@ -197,8 +196,14 @@ func (s *ScriptBuilder) pushData(data interface{}) error { s.RawBytes = append(s.RawBytes, e.Address...) //20 bytes return nil case UTXO: + //remove prefix 0x here + //check if the scripthash is prefixed with 0x. if so, trim it out. + trimmed0x := e.TXID + if has0xPrefix(e.TXID) == true { + trimmed0x = e.TXID[2:] + } //reverse txID to little endian - b, err := hex.DecodeString(e.TXID) + b, err := hex.DecodeString(trimmed0x) if err != nil { return err } @@ -264,7 +269,6 @@ func NewScriptHash(hexString string) (ScriptHash, error) { if has0xPrefix(hexString) == true { trimmed0x = hexString[2:] } - log.Printf("script hash = %v", trimmed0x) b, err := hex.DecodeString(trimmed0x) if err != nil { return nil, err @@ -349,7 +353,6 @@ func (s *ScriptBuilder) GenerateTransactionInput(unspent Unspent, assetToSend Na utxoSumAmount += addingUTXO.Value index += 1 count += 1 - log.Printf("input = %v %v", addingUTXO.TXID, addingUTXO.Value) } //fee input part @@ -397,7 +400,6 @@ func (s *ScriptBuilder) GenerateTransactionOutput(sender NEOAddress, receiver NE if assetToSend == NEO && feeAmount > 0 { needAnotherAssetForFee = true } - log.Printf("needAnotherAssetForFee = %v", needAnotherAssetForFee) if amountToSend > sendingAsset.TotalAmount() { return nil, fmt.Errorf("you don't have enough balance. Sending %v but only have %v", amountToSend, sendingAsset.TotalAmount()) @@ -422,7 +424,6 @@ func (s *ScriptBuilder) GenerateTransactionOutput(sender NEOAddress, receiver NE totalAmountInInputs := utxoSumAmount needTwoOutputTransaction := totalAmountInInputs != amountToSend list := []TransactionOutput{} - log.Printf("needTwoOutputTransaction=%v", needTwoOutputTransaction) if needTwoOutputTransaction { //first output is the amount to send to the receiver @@ -441,7 +442,6 @@ func (s *ScriptBuilder) GenerateTransactionOutput(sender NEOAddress, receiver NE if needAnotherAssetForFee == false && float64(feeAmount) > 0 { returningAmount -= float64(feeAmount) } - log.Printf("returningAmount = %v", returningAmount) //return the left over to sender returningOutput := TransactionOutput{ Asset: assetToSend, diff --git a/neoutils/utils_test.go b/neoutils/utils_test.go index 853d8c4..a1d496b 100644 --- a/neoutils/utils_test.go +++ b/neoutils/utils_test.go @@ -53,7 +53,7 @@ func TestValidateNEOAddressInvalidAddress(t *testing.T) { } func TestConverting(t *testing.T) { - hex := "00e8764817" + hex := "842c720000000000" //hex := "005c7c875e" = 405991873536 value := ConvertByteArrayToBigInt(hex) diff --git a/neoutils/version.go b/neoutils/version.go index 0144bed..b2b7f15 100644 --- a/neoutils/version.go +++ b/neoutils/version.go @@ -1,14 +1,17 @@ package neoutils const ( - VERSION = "1.0.3" + VERSION = "1.0.4" ) //RELEASE NOTES -//V. 1.0.3 -//mintTokens now triggers Verification +// V. 1.0.4 +// - Updated to use UTXO from O3 -//V. 1.0.2 -//- added txid in return -//MintTokensRawTransaction(wallet Wallet, assetToSend smartcontract.NativeAsset, amount float64, unspent smartcontract.Unspent, remark string) ([]byte, string, error) -//TransferNEP5RawTransaction(wallet Wallet, toAddress smartcontract.NEOAddress, amount float64, unspent smartcontract.Unspent, attributes map[smartcontract.TransactionAttribute][]byte) ([]byte, string, error) +// V. 1.0.3 +// - mintTokens now triggers Verification + +// V. 1.0.2 +// - added txid in return +// - MintTokensRawTransaction(wallet Wallet, assetToSend smartcontract.NativeAsset, amount float64, unspent smartcontract.Unspent, remark string) ([]byte, string, error) +// - TransferNEP5RawTransaction(wallet Wallet, toAddress smartcontract.NEOAddress, amount float64, unspent smartcontract.Unspent, attributes map[smartcontract.TransactionAttribute][]byte) ([]byte, string, error)