diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a84b2edd..f7bf5c43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev hwloc libhwloc-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env @@ -83,6 +84,21 @@ jobs: make clean make + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + - name: Run vetting run: | export PATH=$PATH:$(go env GOPATH)/bin @@ -159,6 +175,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev hwloc libhwloc-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env @@ -197,6 +214,21 @@ jobs: make clean make + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + - name: Run vetting run: | export PATH=$PATH:$(go env GOPATH)/bin @@ -284,6 +316,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev hwloc libhwloc-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env @@ -322,6 +355,21 @@ jobs: make clean make + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + - name: Run vetting run: | export PATH=$PATH:$(go env GOPATH)/bin @@ -408,6 +456,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev hwloc libhwloc-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env @@ -446,6 +495,21 @@ jobs: make clean make + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + - name: Run vetting run: | export PATH=$PATH:$(go env GOPATH)/bin @@ -532,6 +596,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev hwloc libhwloc-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env @@ -570,6 +635,21 @@ jobs: make clean make + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + - name: Run vetting run: | export PATH=$PATH:$(go env GOPATH)/bin @@ -656,6 +736,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev hwloc libhwloc-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env @@ -694,6 +775,21 @@ jobs: make clean make + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + - name: Run vetting run: | export PATH=$PATH:$(go env GOPATH)/bin @@ -736,3 +832,138 @@ jobs: -btc=true \ -bch=true \ -timeout 1500s + + test-harmony: + runs-on: ubuntu-latest + env: + FILECOIN_FFI_COMMIT: a62d00da59d1b0fb35f3a4ae854efa9441af892d + SOLANA_FFI_COMMIT: f6521b8a1af44f4d468bc8e7e67ba3766a5602ef + steps: + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + with: + submodules: recursive + + - name: Caching modules + uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-aw-${{ hashFiles('**/go.sum') }} + + - name: Cache extern dependencies (FFI) + id: cache-extern + uses: actions/cache@v2 + env: + cache-name: cache-externs + with: + path: .extern + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.FILECOIN_FFI_COMMIT }}-${{ env.SOLANA_FFI_COMMIT }} + + # Remove apt repos that are known to break from time to time + # See https://github.com/actions/virtual-environments/issues/323 + - name: Install dependency packages + run: | + for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done + sudo apt-get update + sudo apt-get install -y build-essential + sudo apt-get install -y jq mesa-opencl-icd ocl-icd-opencl-dev pkg-config libudev-dev + sudo apt install libgmp-dev libssl-dev make gcc g++ + curl https://sh.rustup.rs -sSf | sh -s -- -y + source $HOME/.cargo/env + + - name: Get dependencies + run: | + export PATH=$PATH:$(go env GOPATH)/bin + source $HOME/.cargo/env + go get -u github.com/onsi/ginkgo/ginkgo + go get -u github.com/onsi/gomega/... + go get -u golang.org/x/lint/golint + go get -u github.com/loongy/covermerge + go get -u github.com/mattn/goveralls + go get -u github.com/xlab/c-for-go + + - name: Install dependencies (Filecoin FFI) + if: steps.cache-extern.outputs.cache-hit != 'true' + run: | + export PATH=$PATH:$(go env GOPATH)/bin + source $HOME/.cargo/env + cd $GITHUB_WORKSPACE + mkdir .extern && cd .extern + git clone https://github.com/filecoin-project/filecoin-ffi.git + cd filecoin-ffi + git checkout ${{ env.FILECOIN_FFI_COMMIT }} + make + + - name: Install dependencies (Solana FFI) + if: steps.cache-extern.outputs.cache-hit != 'true' + run: | + export PATH=$PATH:$(go env GOPATH)/bin + source $HOME/.cargo/env + cd $GITHUB_WORKSPACE/.extern + git clone https://github.com/renproject/solana-ffi.git + cd solana-ffi + git checkout ${{ env.SOLANA_FFI_COMMIT }} + make clean + make + + - name: Install dependencies (Harmony) + run: | + cd $GITHUB_WORKSPACE/chain/harmony + git clone https://github.com/harmony-one/bls.git + git clone https://github.com/harmony-one/mcl.git + cd $GITHUB_WORKSPACE/chain/harmony/bls + make + cd $GITHUB_WORKSPACE + export CGO_CFLAGS="-I$GITHUB_WORKSPACE/chain/harmony/bls/include -I$GITHUB_WORKSPACE/chain/harmony/mcl/include -I/usr/local/opt/openssl/include" + export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/chain/harmony/bls/lib -L/usr/local/opt/openssl/lib" + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/chain/harmony/bls/lib:$GITHUB_WORKSPACE/chain/harmony/mcl/lib:/usr/local/opt/openssl/lib + export LIBRARY_PATH=$LD_LIBRARY_PATH + export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH + export GO111MODULE=on + + - name: Run vetting + run: | + export PATH=$PATH:$(go env GOPATH)/bin + source $HOME/.cargo/env + go mod edit -replace=github.com/filecoin-project/filecoin-ffi=./.extern/filecoin-ffi + go mod edit -replace=github.com/renproject/solana-ffi=./.extern/solana-ffi + go vet ./... + + - name: Run linting + run: | + cd $GITHUB_WORKSPACE + export PATH=$PATH:$(go env GOPATH)/bin + go get -u golang.org/x/lint/golint + golint $(go list ./... | grep -v filecoin-ffi) + + - name: Run multichain infrastructure + run: | + cd $GITHUB_WORKSPACE/infra + source .env + docker-compose up -d --build \ + harmony + + - name: Sleep until the nodes are up + uses: jakejarvis/wait-action@master + with: + time: '1m' + + - name: Check on docker containers + run: docker ps -a + + - name: Run tests and report test coverage + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export PATH=$PATH:$(go env GOPATH)/bin + source ./infra/.env + cd $GITHUB_WORKSPACE + go test \ + -one=true \ + -timeout 1500s diff --git a/chain/harmony/account.go b/chain/harmony/account.go new file mode 100644 index 00000000..87aef0f1 --- /dev/null +++ b/chain/harmony/account.go @@ -0,0 +1,334 @@ +package harmony + +import ( + "context" + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rlp" + "github.com/harmony-one/harmony/core/types" + common2 "github.com/harmony-one/harmony/rpc/common" + "github.com/renproject/multichain/api/account" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/contract" + "github.com/renproject/pack" + "math/big" +) + +const ( + DefaultShardID = 1 + DefaultHost = "http://127.0.0.1:9598" +) + +type TxBuilder struct { + chainID *big.Int +} + +func NewTxBuilder(chainId *big.Int) account.TxBuilder { + return &TxBuilder{ + chainID: chainId, + } +} + +func (txBuilder *TxBuilder) BuildTx(ctx context.Context, from, to address.Address, value, nonce, gasLimit, gasPrice, gasCap pack.U256, payload pack.Bytes) (account.Tx, error) { + toAddr, err := NewEncoderDecoder().DecodeAddress(to) + if err != nil { + return nil, err + } + tx := types.NewTransaction( + nonce.Int().Uint64(), + common.BytesToAddress(toAddr), + DefaultShardID, + value.Int(), + gasLimit.Int().Uint64(), + gasPrice.Int(), + payload) + return &Tx{ + harmonyTx: *tx, + chainId: txBuilder.chainID, + sender: from, + signed: false, + }, nil +} + +type TxData struct { + Blockhash string `json:"blockHash"` + Blocknumber uint64 `json:"blockNumber"` + From string `json:"from"` + Gas uint64 `json:"gas"` + Gasprice *big.Int `json:"gasPrice"` + Hash string `json:"hash"` + Input string `json:"input"` + Nonce uint64 `json:"nonce"` + R string `json:"r"` + S string `json:"s"` + Shardid uint32 `json:"shardID"` + Timestamp uint64 `json:"timestamp"` + To string `json:"to"` + Toshardid uint32 `json:"toShardID"` + Transactionindex uint64 `json:"transactionIndex"` + V string `json:"v"` + Value *big.Int `json:"value"` +} + +type Tx struct { + harmonyTx types.Transaction + chainId *big.Int + sender address.Address + signed bool +} + +func (tx *Tx) Hash() pack.Bytes { + return pack.NewBytes(tx.harmonyTx.Hash().Bytes()) +} + +func (tx *Tx) From() address.Address { + from, err := tx.harmonyTx.SenderAddress() + if err == nil { + addr, err := NewEncoderDecoder().EncodeAddress(from.Bytes()) + if err == nil { + return addr + } + } + return tx.sender +} + +func (tx *Tx) To() address.Address { + to := tx.harmonyTx.To() + if to != nil { + addr, err := NewEncoderDecoder().EncodeAddress(to.Bytes()) + if err == nil { + return addr + } + } + return "" +} + +func (tx *Tx) Value() pack.U256 { + return pack.NewU256FromInt(tx.harmonyTx.Value()) +} + +func (tx *Tx) Nonce() pack.U256 { + return pack.NewU256FromU64(pack.NewU64(tx.harmonyTx.Nonce())) +} + +func (tx *Tx) Payload() contract.CallData { + return tx.harmonyTx.Data() +} + +func (tx *Tx) Sighashes() ([]pack.Bytes32, error) { + const digestLength = 32 + var ( + digestHash [32]byte + sighashes []pack.Bytes32 + ) + h := types.NewEIP155Signer(tx.chainId).Hash(&tx.harmonyTx).Bytes() + if len(h) != digestLength { + return nil, fmt.Errorf("hash is required to be exactly %d bytes (%d)", digestLength, len(h)) + } + copy(digestHash[:], h[:32]) + sighashes = append(sighashes, digestHash) + return sighashes, nil +} + +func (tx *Tx) Sign(signatures []pack.Bytes65, pubKey pack.Bytes) error { + if len(signatures) != 1 { + return fmt.Errorf("expected 1 signature, got %v signatures", len(signatures)) + } + signedTx, err := tx.harmonyTx.WithSignature(types.NewEIP155Signer(tx.chainId), signatures[0].Bytes()) + if err != nil { + return err + } + tx.harmonyTx = *signedTx + tx.signed = true + return nil +} + +func (tx *Tx) Serialize() (pack.Bytes, error) { + serializedTx, err := rlp.EncodeToBytes(&tx.harmonyTx) + if err != nil { + return pack.Bytes{}, err + } + return pack.NewBytes(serializedTx), nil +} + +type ClientOptions struct { + Host string +} + +type Client struct { + opts ClientOptions +} + +func (opts ClientOptions) WithHost(host string) ClientOptions { + opts.Host = host + return opts +} + +func DefaultClientOptions() ClientOptions { + return ClientOptions{ + Host: DefaultHost, + } +} + +func NewClient(opts ClientOptions) *Client { + return &Client{opts: opts} +} + +func (c *Client) LatestBlock(ctx context.Context) (pack.U64, error) { + for { + select { + case <-ctx.Done(): + return pack.NewU64(0), ctx.Err() + default: + } + const method = "hmyv2_blockNumber" + response, err := SendData(method, []byte{}, c.opts.Host) + if err != nil { + fmt.Println(err) + return pack.NewU64(0), err + } + var latestBlock uint64 + if err := json.Unmarshal(*response.Result, &latestBlock); err != nil { + return pack.NewU64(0), fmt.Errorf("decoding result: %v", err) + } + return pack.NewU64(latestBlock), nil + } +} + +func (c *Client) AccountBalance(ctx context.Context, addr address.Address) (pack.U256, error) { + for { + select { + case <-ctx.Done(): + return pack.U256{}, ctx.Err() + default: + } + data := []byte(fmt.Sprintf("[\"%s\"]", addr)) + const method = "hmyv2_getBalance" + response, err := SendData(method, data, c.opts.Host) + if err != nil { + fmt.Println(err) + return pack.U256{}, err + } + var balance uint64 + if err := json.Unmarshal(*response.Result, &balance); err != nil { + return pack.U256{}, fmt.Errorf("decoding result: %v", err) + } + return pack.NewU256FromU64(pack.NewU64(balance)), nil + } +} + +func (c *Client) AccountNonce(ctx context.Context, addr address.Address) (pack.U256, error) { + for { + select { + case <-ctx.Done(): + return pack.U256{}, ctx.Err() + default: + } + data := []byte(fmt.Sprintf("[\"%s\", \"%s\"]", addr, "SENT")) + const method = "hmyv2_getTransactionsCount" + response, err := SendData(method, data, c.opts.Host) + if err != nil { + fmt.Println(err) + return pack.U256{}, err + } + var nonce uint64 + if err := json.Unmarshal(*response.Result, &nonce); err != nil { + return pack.U256{}, fmt.Errorf("decoding result: %v", err) + } + return pack.NewU256FromU64(pack.NewU64(nonce)), nil + } +} + +func (c *Client) Tx(ctx context.Context, hash pack.Bytes) (account.Tx, pack.U64, error) { + for { + select { + case <-ctx.Done(): + return nil, pack.NewU64(0), ctx.Err() + default: + } + data := []byte(fmt.Sprintf("[\"%s\"]", hexutil.Encode(hash))) + const method = "hmyv2_getTransactionByHash" + response, err := SendData(method, data, c.opts.Host) + if err != nil { + return nil, pack.NewU64(0), err + } + var txData TxData + if response.Result == nil { + return nil, pack.NewU64(0), fmt.Errorf("decoding result: %v", err) + } + if err := json.Unmarshal(*response.Result, &txData); err != nil { + return nil, pack.NewU64(0), fmt.Errorf("decoding result: %v", err) + } + + tx, err := buildTxFromTxData(txData) + return tx, pack.NewU64(1), err + } +} + +func (c *Client) SubmitTx(ctx context.Context, tx account.Tx) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + txSerilized, err := tx.Serialize() + if err != nil { + return err + } + hexSignature := hexutil.Encode(txSerilized) + data := []byte(fmt.Sprintf("[\"%s\"]", hexSignature)) + const method = "hmyv2_sendRawTransaction" + tx1 := new(types.Transaction) + err = rlp.DecodeBytes(txSerilized, tx1) + _, err = SendData(method, data, c.opts.Host) + if err != nil { + return err + } + return nil + } +} + +func (c *Client) ChainId(ctx context.Context) (*big.Int, error) { + for { + select { + case <-ctx.Done(): + return big.NewInt(0), ctx.Err() + default: + } + const method = "hmyv2_getNodeMetadata" + response, err := SendData(method, []byte{}, c.opts.Host) + if err != nil { + fmt.Println(err) + return big.NewInt(0), err + } + var nodeMetadata common2.NodeMetadata + if err := json.Unmarshal(*response.Result, &nodeMetadata); err != nil { + return big.NewInt(0), fmt.Errorf("decoding result: %v", err) + } + return nodeMetadata.ChainConfig.ChainID, nil + } +} + +func buildTxFromTxData(data TxData) (account.Tx, error) { + toAddr, err := NewEncoderDecoder().DecodeAddress(address.Address(data.To)) + if err != nil { + return nil, err + } + tx := types.NewTransaction( + data.Nonce, + common.BytesToAddress(toAddr), + data.Shardid, + data.Value, + data.Gas, + data.Gasprice, + pack.Bytes(nil), + ) + return &Tx{ + harmonyTx: *tx, + sender: address.Address(data.From), + signed: true, + }, nil +} \ No newline at end of file diff --git a/chain/harmony/account_test.go b/chain/harmony/account_test.go new file mode 100644 index 00000000..8fb97af2 --- /dev/null +++ b/chain/harmony/account_test.go @@ -0,0 +1,83 @@ +package harmony_test + +import ( + "context" + "github.com/btcsuite/btcutil/bech32" + "github.com/ethereum/go-ethereum/crypto" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/chain/harmony" + "github.com/renproject/pack" + "time" +) + +var _ = Describe("Harmony", func() { + Context("when broadcasting a tx", func() { + It("should work", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + c := harmony.NewClient(harmony.DefaultClientOptions()) + chainId, err := c.ChainId(ctx) + Expect(err).NotTo(HaveOccurred()) + + x := "1f84c95ac16e6a50f08d44c7bde7aff8742212fda6e4321fde48bf83bef266dc" + senderKey, err := crypto.HexToECDSA(x) + Expect(err).NotTo(HaveOccurred()) + addrBytes, err := bech32.ConvertBits(crypto.PubkeyToAddress(senderKey.PublicKey).Bytes(), 8, 5, true) + Expect(err).NotTo(HaveOccurred()) + senderAddr, err := bech32.Encode(harmony.Bech32AddressHRP, addrBytes) + Expect(err).NotTo(HaveOccurred()) + + toKey, _ := crypto.GenerateKey() + toAddrBytes, err := bech32.ConvertBits(crypto.PubkeyToAddress(toKey.PublicKey).Bytes(), 8, 5, true) + Expect(err).NotTo(HaveOccurred()) + toAddr, err := bech32.Encode(harmony.Bech32AddressHRP, toAddrBytes) + Expect(err).NotTo(HaveOccurred()) + + nonce, err := c.AccountNonce(ctx, address.Address(senderAddr)) + Expect(err).NotTo(HaveOccurred()) + + gasLimit := uint64(80000000) + gas, err := harmony.Estimator{}.EstimateGasPrice(ctx) + Expect(err).NotTo(HaveOccurred()) + + amount := pack.NewU256FromU64(pack.NewU64(100000000)) + + txBuilder := harmony.NewTxBuilder(chainId) + tx, err := txBuilder.BuildTx(ctx, address.Address(senderAddr), address.Address(toAddr), amount, nonce, pack.NewU256FromU64(pack.NewU64(gasLimit)), gas, gas, pack.Bytes(nil)) + Expect(err).NotTo(HaveOccurred()) + + sigHash, err := tx.Sighashes() + Expect(err).ToNot(HaveOccurred()) + Expect(len(sigHash)).To(Equal(1)) + + sig, err := crypto.Sign(sigHash[0][:], senderKey) + Expect(err).ToNot(HaveOccurred()) + Expect(len(sig)).To(Equal(65)) + + var signature [65]byte + copy(signature[:], sig) + err = tx.Sign([]pack.Bytes65{pack.NewBytes65(signature)}, pack.Bytes(nil)) + Expect(err).ToNot(HaveOccurred()) + + err = c.SubmitTx(ctx, tx) + Expect(err).ToNot(HaveOccurred()) + + time.Sleep(time.Second) + for { + txResp, _, err := c.Tx(ctx, tx.Hash()) + if err == nil && txResp != nil { + break + } + // wait and retry querying for the transaction + time.Sleep(5 * time.Second) + } + + updatedBalance, err := c.AccountBalance(ctx, address.Address(toAddr)) + Expect(err).NotTo(HaveOccurred()) + Expect(updatedBalance).To(Equal(amount)) + + }) + }) +}) diff --git a/chain/harmony/address.go b/chain/harmony/address.go new file mode 100644 index 00000000..28e50e1e --- /dev/null +++ b/chain/harmony/address.go @@ -0,0 +1,56 @@ +package harmony + +import ( + "github.com/btcsuite/btcutil/bech32" + "github.com/renproject/multichain/api/address" +) + +const Bech32AddressHRP = "one" + +type EncoderDecoder struct { + address.Encoder + address.Decoder +} + +func NewEncoderDecoder() address.EncodeDecoder { + return EncoderDecoder{ + Encoder: NewEncoder(), + Decoder: NewDecoder(), + } +} + +type Encoder struct{} + +func (Encoder) EncodeAddress(addr address.RawAddress) (address.Address, error) { + converted, err := bech32.ConvertBits(addr, 8, 5, true) + if err != nil { + return "", err + } + encodedAddr, err := bech32.Encode(Bech32AddressHRP, converted) + if err != nil { + return "", err + } + return address.Address(encodedAddr), nil +} + +type Decoder struct{} + +func (Decoder) DecodeAddress(addr address.Address) (address.RawAddress, error) { + _, decodedAddr, err := bech32.Decode(string(addr)) + if err != nil { + return nil, err + } + converted, err := bech32.ConvertBits(decodedAddr, 5, 8, false) + if err != nil { + return nil, err + } + return converted, nil +} + +func NewEncoder() address.Encoder { + return Encoder{} +} + +func NewDecoder() address.Decoder { + return Decoder{} +} \ No newline at end of file diff --git a/chain/harmony/address_test.go b/chain/harmony/address_test.go new file mode 100644 index 00000000..284db70d --- /dev/null +++ b/chain/harmony/address_test.go @@ -0,0 +1,76 @@ +package harmony_test + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/chain/harmony" +) + +var _ = Describe("Address", func() { + Context("when decoding a valid address", func() { + It("should work without errors", func() { + addrs := []string{ + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "A12UEL5L", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + } + encoderDecoder := harmony.NewEncoderDecoder() + for _, addr := range addrs { + _, err := encoderDecoder.DecodeAddress(address.Address(addr)) + Expect(err).ToNot(HaveOccurred()) + } + }) + }) + + Context("when decoding an invalid address", func() { + It("should work without errors", func() { + addrs := []string{ + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", + "s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", + "spl" + string(rune(127)) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + "split1cheo2y9e2w", + "split1a2y9w", + "1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + } + for _, addr := range addrs { + _, err := harmony.NewEncoderDecoder().DecodeAddress(address.Address(addr)) + Expect(err).To(HaveOccurred()) + } + }) + }) + + Context("when encoding a valid address", func() { + It("should work without errors", func() { + key, _ := crypto.GenerateKey() + ethAddr := crypto.PubkeyToAddress(key.PublicKey).String() + encoderDecoder := harmony.NewEncoderDecoder() + addr, err := encoderDecoder.EncodeAddress(common.HexToAddress(ethAddr).Bytes()) + Expect(err).ToNot(HaveOccurred()) + + rawAddr, err := encoderDecoder.DecodeAddress(addr) + Expect(err).ToNot(HaveOccurred()) + for i, b := range rawAddr { + Expect(b).To(Equal(common.HexToAddress(ethAddr).Bytes()[i])) + } + }) + }) + + Context("when encoding/decoding a valid address", func() { + It("should work without errors", func() { + addr := address.Address("one1zksj3evekayy90xt4psrz8h6j2v3hla4qwz4ur") + encoderDecoder := harmony.NewEncoderDecoder() + rawAddr, err := encoderDecoder.DecodeAddress(addr) + Expect(err).ToNot(HaveOccurred()) + + conv, err := encoderDecoder.EncodeAddress(rawAddr) + Expect(err).ToNot(HaveOccurred()) + Expect(conv).To(Equal(addr)) + }) + }) +}) \ No newline at end of file diff --git a/chain/harmony/contract.go b/chain/harmony/contract.go new file mode 100644 index 00000000..0e7b086c --- /dev/null +++ b/chain/harmony/contract.go @@ -0,0 +1,45 @@ +package harmony + +import ( + "context" + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/contract" + "github.com/renproject/pack" +) + +type CallArgs struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` +} +type Params struct { + CallArgs CallArgs + Block uint64 +} + +func (c *Client) CallContract(ctx context.Context, addr address.Address, callData contract.CallData) (pack.Bytes, error) { + const method = "hmyv2_call" + // Unmarshal required to get the block number parameter for the call + var callParams Params + err := json.Unmarshal(callData, &callParams) + if err != nil { + return nil, err + } + args, err := json.Marshal(callParams.CallArgs) + if err != nil { + return nil, err + } + data := []byte(fmt.Sprintf("[%s, %d]", string(args), callParams.Block)) + response, err := SendData(method, data, c.opts.Host) + if err != nil { + return nil, err + } + return pack.NewBytes(*response.Result), nil +} \ No newline at end of file diff --git a/chain/harmony/contract_test.go b/chain/harmony/contract_test.go new file mode 100644 index 00000000..2902492a --- /dev/null +++ b/chain/harmony/contract_test.go @@ -0,0 +1,34 @@ +package harmony_test + +import ( + "context" + "encoding/json" + "github.com/ethereum/go-ethereum/common" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/chain/harmony" +) + +var _ = Describe("Harmony", func() { + Context("when calling a contract", func() { + It("should work", func() { + contractAddr := address.Address("one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3") + rawAddr, err := harmony.NewEncoderDecoder().DecodeAddress(contractAddr) + bech32Addr := common.BytesToAddress(rawAddr) + callData := harmony.CallArgs{ + To: &bech32Addr, + } + params := harmony.Params{ + CallArgs: callData, + Block: 37000, + } + marshalledData, err := json.Marshal(params) + Expect(err).NotTo(HaveOccurred()) + + c := harmony.NewClient(harmony.DefaultClientOptions()) + _, err = c.CallContract(context.TODO(), contractAddr, marshalledData) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) \ No newline at end of file diff --git a/chain/harmony/gas.go b/chain/harmony/gas.go new file mode 100644 index 00000000..8b229592 --- /dev/null +++ b/chain/harmony/gas.go @@ -0,0 +1,21 @@ +package harmony + +import( + "context" + "math/big" + + "github.com/renproject/pack" +) + +var ( + defaultGas = big.NewInt(1) +) + +type Estimator struct {} + +// The average block time on Harmony is 5 seconds & each block has a max +// gas limit of 80 million. There is currently no need to estimate gas for +// regular transactions & we do not have the RPC for it. +func (Estimator) EstimateGasPrice(ctx context.Context) (pack.U256, error) { + return pack.NewU256FromInt(defaultGas), nil +} diff --git a/chain/harmony/gas_test.go b/chain/harmony/gas_test.go new file mode 100644 index 00000000..1c1ee60e --- /dev/null +++ b/chain/harmony/gas_test.go @@ -0,0 +1,20 @@ +package harmony_test + +import ( + "context" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/renproject/multichain/chain/harmony" + "github.com/renproject/pack" +) + +var _ = Describe("Gas", func() { + Context("when estimating gas", func() { + It("should work without errors", func() { + e := harmony.Estimator{} + gas, err := e.EstimateGasPrice(context.TODO()) + Expect(err).NotTo(HaveOccurred()) + Expect(gas).To(Equal(pack.NewU256FromU64(pack.NewU64(1)))) + }) + }) +}) diff --git a/chain/harmony/harmony_suite_test.go b/chain/harmony/harmony_suite_test.go new file mode 100644 index 00000000..2578be39 --- /dev/null +++ b/chain/harmony/harmony_suite_test.go @@ -0,0 +1,12 @@ +package harmony + +import "testing" +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestHarmony(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Harmony Suite") +} \ No newline at end of file diff --git a/chain/harmony/rpc.go b/chain/harmony/rpc.go new file mode 100644 index 00000000..88333e57 --- /dev/null +++ b/chain/harmony/rpc.go @@ -0,0 +1,154 @@ +package harmony + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "time" +) + +// Request defines a JSON-RPC 2.0 request object. See +// https://www.jsonrpc.org/specification for more information. A Request should +// not be explicitly created, but instead unmarshaled from JSON. +type Request struct { + Version string `json:"jsonrpc"` + ID interface{} `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +// Response defines a JSON-RPC 2.0 response object. See +// https://www.jsonrpc.org/specification for more information. A Response is +// usually marshaled into bytes and returned in response to a Request. +type Response struct { + Version string `json:"jsonrpc"` + ID interface{} `json:"id"` + Result *json.RawMessage `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` +} + +// Error defines a JSON-RPC 2.0 error object. See +// https://www.jsonrpc.org/specification for more information. +type Error struct { + Code int `json:"code"` + Message string `json:"message"` + Data *json.RawMessage `json:"data"` +} + +// SendData sends data to method via jsonrpc +func SendData(method string, data []byte, url string) (Response, error) { + request := Request{ + Version: "2.0", + ID: 1, + Method: method, + Params: data, + } + // Send request to lightnode + response, err := SendRequest(request, url) + if err != nil { + return Response{}, err + } + + var resp Response + buf := new(bytes.Buffer) + buf.ReadFrom(response.Body) + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + return Response{}, fmt.Errorf("cannot decode %v response body = %s, err = %v", method, buf.String(), err) + } + if resp.Error != nil { + return Response{}, fmt.Errorf("got err back from %v request, err = %v", method, resp.Error) + } + return resp, nil +} + +// SendDataWithRetry is the same as SendData but will retry if sending the request failed +func SendDataWithRetry(method string, data []byte, url string) (Response, error) { + request := Request{ + Version: "2.0", + ID: 1, + Method: method, + Params: data, + } + // Send request to lightnode with retry (max 10 times) + response, err := SendRequestWithRetry(request, url, 10, 10) + if err != nil { + return Response{}, fmt.Errorf("failed to send request, err = %v", err) + } + + var resp Response + buf := new(bytes.Buffer) + buf.ReadFrom(response.Body) + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + return Response{}, fmt.Errorf("cannot decode %v response body = %s, err = %v", method, buf.String(), err) + } + if resp.Error != nil { + return Response{}, fmt.Errorf("got err back from %v request, err = %v", method, resp.Error) + } + return resp, nil +} + +// SendRequest sends the JSON-2.0 request to the target url and returns the response and any error. +func SendRequest(request Request, url string) (*http.Response, error) { + data, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := SendRawPost(data, url) + if err != nil { + fmt.Printf("Sending %s to %s resulted in an error: %v\n", string(data), url, err) + return nil, err + } + return resp, nil +} + +// SendRawPost sends a raw bytes as a POST request to the URL specified +func SendRawPost(data []byte, url string) (*http.Response, error) { + if !strings.HasPrefix(url, "http") { + url = "http://" + url + } + client := newClient(10 * time.Second) + buff := bytes.NewBuffer(data) + req, err := http.NewRequest("POST", url, buff) + req.Header.Set("Content-Type", "application/json") + if err != nil { + return nil, err + } + return client.Do(req) +} + +// SendRequestWithRetry calls SendRequest but with configurable retry logic +func SendRequestWithRetry(request Request, url string, timeoutInSecs int, retries int) (response *http.Response, err error) { + failures := 0 + for failures < retries { + response, err = SendRequest(request, url) + if err != nil { + failures++ + if failures >= retries { + return nil, err + } + fmt.Printf("%s errored: %v. Retrying after %d seconds\n", url, err, timeoutInSecs) + time.Sleep(time.Duration(timeoutInSecs) * time.Second) + continue + } + break + } + return +} + +func newClient(timeout time.Duration) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 2 * time.Second, + KeepAlive: 10 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 4 * time.Second, + ResponseHeaderTimeout: 3 * time.Second, + }, + Timeout: timeout, + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 24c72c4b..317d4015 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/filecoin-project/go-jsonrpc v0.1.4-0.20210217175800-45ea43ac2bec github.com/filecoin-project/go-state-types v0.1.1-0.20210506134452-99b279731c48 github.com/filecoin-project/lotus v1.9.0 + github.com/harmony-one/harmony v1.10.2 github.com/ipfs/go-cid v0.0.7 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 github.com/multiformats/go-varint v0.0.6 @@ -37,3 +38,4 @@ replace github.com/filecoin-project/filecoin-ffi => ./chain/filecoin/filecoin-ff replace github.com/renproject/solana-ffi => ./chain/solana/solana-ffi replace github.com/keybase/go-keychain => github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 + diff --git a/infra/.env b/infra/.env index 7d32d6e5..75dd3d0e 100644 --- a/infra/.env +++ b/infra/.env @@ -58,6 +58,13 @@ export ETHEREUM_ADDRESS=0xa0df350d2637096571F7A701CBc1C5fdE30dF76A export FANTOM_PK=163f5f0f9a621d72fedd85ffca3d08d131ab4e812181e0d30ffd1c885d20aac7 export FANTOM_ADDRESS=0x239fA7623354eC26520dE878B52f13Fe84b06971 +# +# Harmony +# + +export HARMONY_PK=1f84c95ac16e6a50f08d44c7bde7aff8742212fda6e4321fde48bf83bef266dc +export HARMONY_ADDRESS=one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3 + # # Filecoin # diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index 402fe7ec..9c5dc344 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -88,6 +88,20 @@ services: - "${ETHEREUM_MNEMONIC}" - "${ETHEREUM_ADDRESS}" + # + # Harmony + # + harmony: + build: + context: ./harmony + ports: + - "0.0.0.0:9500:9500" + - "0.0.0.0:9501:9501" + - "0.0.0.0:9599:9599" + - "0.0.0.0:9598:9598" + entrypoint: + - "./root/run.sh" + # # Fantom # diff --git a/infra/harmony/Dockerfile b/infra/harmony/Dockerfile new file mode 100644 index 00000000..85cc5e3e --- /dev/null +++ b/infra/harmony/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.14 + +WORKDIR $GOPATH/src/github.com/harmony-one + +RUN apt update && apt upgrade -y && apt update -y && apt install unzip \ +libgmp-dev libssl-dev curl git jq make gcc g++ bash sudo -y + +RUN git clone https://github.com/harmony-one/harmony.git +RUN git clone https://github.com/harmony-one/bls.git +RUN git clone https://github.com/harmony-one/mcl.git +RUN cd harmony && make + +COPY run.sh /root/run.sh +RUN chmod +x /root/run.sh + +EXPOSE 9500 +EXPOSE 9501 +EXPOSE 9599 +EXPOSE 9598 + +WORKDIR / + +ENTRYPOINT ["./root/run.sh"] \ No newline at end of file diff --git a/infra/harmony/run.sh b/infra/harmony/run.sh new file mode 100644 index 00000000..a4a0a9a4 --- /dev/null +++ b/infra/harmony/run.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e + +cd "$GOPATH/src/github.com/harmony-one/harmony" +bash ./test/deploy.sh -B -D 60000 ./test/configs/local-resharding.txt \ No newline at end of file diff --git a/multichain.go b/multichain.go index ef2eee7e..b9732ead 100644 --- a/multichain.go +++ b/multichain.go @@ -112,6 +112,7 @@ const ( LUNA = Asset("LUNA") // Luna MATIC = Asset("MATIC") // Matic PoS (Polygon) SOL = Asset("SOL") // Solana + ONE = Asset("ONE") // Harmony ZEC = Asset("ZEC") // Zcash // These assets are defined separately because they are mock assets. These @@ -152,6 +153,8 @@ func (asset Asset) OriginChain() Chain { return Polygon case SOL: return Solana + case ONE: + return Harmony case ZEC: return Zcash @@ -175,7 +178,7 @@ func (asset Asset) ChainType() ChainType { switch asset { case BCH, BTC, DGB, DOGE, ZEC: return ChainTypeUTXOBased - case AVAX, BNB, ETH, FIL, GLMR, LUNA, MATIC: + case AVAX, BNB, ETH, FIL, GLMR, LUNA, MATIC, ONE: return ChainTypeAccountBased // These assets are handled separately because they are mock assets. These @@ -223,6 +226,7 @@ const ( Ethereum = Chain("Ethereum") Fantom = Chain("Fantom") Filecoin = Chain("Filecoin") + Harmony = Chain("Harmony") Moonbeam = Chain("Moonbeam") Polygon = Chain("Polygon") Solana = Chain("Solana") @@ -261,7 +265,7 @@ func (chain Chain) ChainType() ChainType { switch chain { case Bitcoin, BitcoinCash, DigiByte, Dogecoin, Zcash: return ChainTypeUTXOBased - case Avalanche, BinanceSmartChain, Ethereum, Fantom, Filecoin, Moonbeam, Polygon, Solana, Terra: + case Avalanche, BinanceSmartChain, Ethereum, Fantom, Filecoin, Moonbeam, Polygon, Solana, Harmony, Terra: return ChainTypeAccountBased // These chains are handled separately because they are mock chains. These @@ -315,6 +319,8 @@ func (chain Chain) NativeAsset() Asset { return GLMR case Polygon: return MATIC + case Harmony: + return ONE case Solana: return SOL case Terra: diff --git a/multichain_test.go b/multichain_test.go index cff4cb1c..8148d35b 100644 --- a/multichain_test.go +++ b/multichain_test.go @@ -28,9 +28,13 @@ import ( "github.com/renproject/multichain/chain/bitcoin" "github.com/renproject/multichain/chain/bitcoincash" + "github.com/btcsuite/btcutil/bech32" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" // "github.com/renproject/multichain/chain/digibyte" "github.com/renproject/multichain/chain/dogecoin" "github.com/renproject/multichain/chain/filecoin" + "github.com/renproject/multichain/chain/harmony" "github.com/renproject/multichain/chain/terra" "github.com/renproject/multichain/chain/zcash" "github.com/renproject/pack" @@ -48,6 +52,7 @@ var ( testBCH = flag.Bool("bch", false, "Pass this flag to test Bitcoincash") testDOGE = flag.Bool("doge", false, "Pass this flag to test Dogecoin") testFIL = flag.Bool("fil", false, "Pass this flag to test Filecoin") + testONE = flag.Bool("one", false, "Pass this flag to test Harmony") testLUNA = flag.Bool("luna", false, "Pass this flag to test Terra") testZEC = flag.Bool("zec", false, "Pass this flag to test Zcash") ) @@ -71,6 +76,7 @@ var _ = Describe("Multichain", func() { testFlags[multichain.BitcoinCash] = *testBCH testFlags[multichain.Dogecoin] = *testDOGE testFlags[multichain.Filecoin] = *testFIL + testFlags[multichain.Harmony] = *testONE testFlags[multichain.Terra] = *testLUNA testFlags[multichain.Zcash] = *testZEC @@ -103,6 +109,10 @@ var _ = Describe("Multichain", func() { multichain.Polygon, multichain.MATIC, }, + { + multichain.Harmony, + multichain.ONE, + }, { multichain.Solana, multichain.SOL, @@ -351,6 +361,33 @@ var _ = Describe("Multichain", func() { return multichain.RawAddress(pack.Bytes(base58.Decode(addrScriptHash.EncodeAddress()))) }, }, + { + multichain.Harmony, + func() multichain.AddressEncodeDecoder { + return harmony.NewEncoderDecoder() + }, + func() multichain.Address { + key, _ := crypto.GenerateKey() + addrBytes := crypto.PubkeyToAddress(key.PublicKey) + conv, err := bech32.ConvertBits(addrBytes.Bytes(), 8, 5, true) + Expect(err).NotTo(HaveOccurred()) + addr, err := bech32.Encode(harmony.Bech32AddressHRP, conv) + Expect(err).NotTo(HaveOccurred()) + return multichain.Address(addr) + }, + func() multichain.RawAddress { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey).String() + Expect(err).NotTo(HaveOccurred()) + return common.HexToAddress(addr).Bytes() + }, + func() multichain.Address { + return multichain.Address("") + }, + func() multichain.RawAddress { + return multichain.RawAddress([]byte{}) + }, + }, } for _, chain := range chainTable { @@ -594,6 +631,53 @@ var _ = Describe("Multichain", func() { }, multichain.Filecoin, }, + { + func() (id.PrivKey, *id.PubKey, multichain.Address) { + pkEnv := os.Getenv("HARMONY_PK") + if pkEnv == "" { + panic("HARMONY_PK is undefined") + } + senderKey, err := crypto.HexToECDSA(pkEnv) + Expect(err).NotTo(HaveOccurred()) + addrBytes, err := bech32.ConvertBits(crypto.PubkeyToAddress(senderKey.PublicKey).Bytes(), 8, 5, true) + Expect(err).NotTo(HaveOccurred()) + addr, err := bech32.Encode(harmony.Bech32AddressHRP, addrBytes) + Expect(err).NotTo(HaveOccurred()) + return id.PrivKey(*senderKey), (*id.PubKey)(&senderKey.PublicKey), multichain.Address(addr) + }, + func(privKey id.PrivKey) multichain.Address { + addrBytes, err := bech32.ConvertBits(crypto.PubkeyToAddress(privKey.PublicKey).Bytes(), 8, 5, true) + Expect(err).NotTo(HaveOccurred()) + addr, err := bech32.Encode(harmony.Bech32AddressHRP, addrBytes) + Expect(err).NotTo(HaveOccurred()) + return multichain.Address(addr) + }, + "http://127.0.0.1:9598", + func() multichain.Address { + toKey, _ := crypto.GenerateKey() + toAddrBytes, err := bech32.ConvertBits(crypto.PubkeyToAddress(toKey.PublicKey).Bytes(), 8, 5, true) + Expect(err).NotTo(HaveOccurred()) + toAddr, err := bech32.Encode(harmony.Bech32AddressHRP, toAddrBytes) + Expect(err).NotTo(HaveOccurred()) + return multichain.Address(toAddr) + }, + func(rpcURL pack.String) (multichain.AccountClient, multichain.AccountTxBuilder) { + client := harmony.NewClient(harmony.DefaultClientOptions()) + chainId, err := client.ChainId(ctx) + Expect(err).NotTo(HaveOccurred()) + txBuilder := harmony.NewTxBuilder(chainId) + return client, txBuilder + }, + func(client multichain.AccountClient) (pack.U256, pack.U256, pack.U256, pack.U256, pack.Bytes) { + gasLimit := uint64(80000000) + gas, err := harmony.Estimator{}.EstimateGasPrice(ctx) + Expect(err).NotTo(HaveOccurred()) + + amount := pack.NewU256FromU64(pack.NewU64(100000000)) + return amount, pack.NewU256FromU64(pack.NewU64(gasLimit)), gas, gas, pack.Bytes(nil) + }, + multichain.Harmony, + }, } for _, accountChain := range accountChainTable {