Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
*.test

# Outputs from tools
*.out
*.out
/.idea
40 changes: 40 additions & 0 deletions chain/waves/address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package waves

import (
"github.com/renproject/multichain/api/address"
"github.com/wavesplatform/gowaves/pkg/proto"
)

type AddressDecoder struct{}

// NewAddressDecoder returns the default AddressDecoder for Waves chain. It
// uses the base58 alphabet to decode the string, and interprets the
// result as a 26-byte array.
func NewAddressDecoder() AddressDecoder {
return AddressDecoder{}
}

// DecodeAddress decodes from string into bytes.
func (AddressDecoder) DecodeAddress(encoded address.Address) (address.RawAddress, error) {
addr, err := proto.NewAddressFromString(string(encoded))
if err != nil {
return nil, err
}
return address.RawAddress(addr.Bytes()), nil
}

type AddressEncoder struct{}

// DecodeAddress decodes from bytes into string.
func (AddressEncoder) EncodeAddress(decoded address.RawAddress) (address.Address, error) {
addr, err := proto.NewAddressFromBytes(decoded)
if err != nil {
return "", err
}
return address.Address(addr.String()), nil
}

type AddressEncodeDecoder struct {
AddressDecoder
AddressEncoder
}
23 changes: 23 additions & 0 deletions chain/waves/address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package waves

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/renproject/multichain/api/address"
)

var _ = Describe("Address", func() {
Context("when decoding and encoding", func() {
It("should equal itself", func() {
addr := address.Address("3PMtE788h78hf1DFVPPXKBVa58sjt3QLxwT")

dec, err := AddressEncodeDecoder{}.DecodeAddress(addr)
Expect(err).ToNot(HaveOccurred())

addr2, err := AddressEncodeDecoder{}.EncodeAddress(dec)
Expect(err).ToNot(HaveOccurred())

Expect(addr).Should(Equal(addr2))
})
})
})
54 changes: 54 additions & 0 deletions chain/waves/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package waves

import (
"time"

"github.com/renproject/multichain/api/address"
"github.com/renproject/pack"
"github.com/wavesplatform/gowaves/pkg/crypto"
"github.com/wavesplatform/gowaves/pkg/proto"
)

// Waves requires public key in from field.
type PublicKey = string

// Helper for creating transactions.
type TxBuilder struct {
chainID byte
}

func NewTxBuilder(chainID byte) *TxBuilder {
return &TxBuilder{
chainID: chainID,
}
}

// BuildTx accepts public key first argument. All other is the same.
func (a TxBuilder) BuildTx(from PublicKey, to address.Address, value, nonce pack.U256, payload pack.Bytes) (Tx, error) {
// Now is the same as nonce, just unique value to distinguish transactions from each other.
now := proto.NewTimestampFromTime(time.Now())
amount := value.Int().Uint64()
rec, err := proto.NewRecipientFromString(string(to))
if err != nil {
return nil, err
}
pub, err := crypto.NewPublicKeyFromBase58(from)
if err != nil {
return nil, err
}
tx := proto.NewUnsignedTransferWithProofs(
3, // last version
pub,
// if empty struct default asset will be used (Waves). Is need some other asset,
// proto.NewOptionalAssetFromDigest(), proto.NewOptionalAssetFromBytes(), proto.NewOptionalAssetFromString() can be used.
// It accepts asset unique id (transaction id), https://wavesexplorer.com/tx/4oZLvmLC1jZQXRSw97c2k4VfK6HgAFXXjWvHFTNH4j2t as example.
proto.OptionalAsset{},
proto.OptionalAsset{},
now,
amount,
100000, //minimal fee
rec,
nil,
)
return newTx(tx, a.chainID)
}
104 changes: 104 additions & 0 deletions chain/waves/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package waves

import (
"context"

"github.com/golang/protobuf/ptypes/empty"
"github.com/pkg/errors"
"github.com/renproject/pack"
g "github.com/wavesplatform/gowaves/pkg/grpc/generated/waves/node/grpc"
"github.com/wavesplatform/gowaves/pkg/proto"
"google.golang.org/grpc"
)

// Provide communication with node through grpc.
type ClientImpl struct {
addr string
chainID proto.Scheme
}

func NewClient(addr string, chainID proto.Scheme) *ClientImpl {
return &ClientImpl{
addr: addr,
chainID: chainID,
}
}

// Retrieve tx by id.
func (a ClientImpl) Tx(ctx context.Context, id pack.Bytes) (Tx, pack.U64, error) {
conn, err := grpc.Dial(a.addr, grpc.WithInsecure())
if err != nil {
return nil, 0, err
}
defer func() {
_ = conn.Close()
}()
txReq, err := g.NewTransactionsApiClient(conn).GetTransactions(ctx, &g.TransactionsRequest{
TransactionIds: [][]byte{id},
})
if err != nil {
return nil, 0, errors.Wrap(err, "failed to request transaction")
}
txResp, err := txReq.Recv()
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get transaction")
}
transfer := txResp.Transaction.Transaction.GetTransfer()
if transfer == nil {
return nil, 0, errors.Errorf(
"expected transaction to be '*Transaction_Transfer', got '%T'",
txResp.Transaction.Transaction)
}
heightReq, err := g.NewBlocksApiClient(conn).GetCurrentHeight(ctx, &empty.Empty{})
if err != nil {
return nil, 0, err
}
height := heightReq.GetValue()
if height == 0 {
return nil, 0, errors.New("invalid value '0' for current blockchain height")
}
if txResp.Height > int64(height) {
return nil, 0, errors.New("height changed during requests")
}
var c proto.ProtobufConverter
tx, err := c.SignedTransaction(txResp.Transaction)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to build transaction from protobuf")
}
casted, ok := tx.(*proto.TransferWithProofs)
if !ok {
return nil, 0, errors.Errorf("expected transaction to be '*proto.TransferWithProofs', got %T", tx)
}
out, err := newTx(casted, a.chainID)
if err != nil {
return nil, 0, err
}
return out, pack.NewU64(uint64(height) - uint64(txResp.Height)), nil
}

// Send transaction to node.
func (a ClientImpl) SubmitTx(ctx context.Context, tx Tx) error {
conn, err := grpc.Dial(a.addr, grpc.WithInsecure())
if err != nil {
return err
}
defer func() {
_ = conn.Close()
}()

original, ok := tx.(OriginalTx)
if !ok {
return errors.New("expected `tx` to be instance of OriginalTx")
}
transfer := original.OriginalTx()
pb, err := transfer.ToProtobufSigned(a.chainID)
if err != nil {
return err
}

_, err = g.NewTransactionsApiClient(conn).Broadcast(ctx, pb)
if err != nil {
return errors.Wrap(err, "failed to broadcast transaction")
}
return err
}
20 changes: 20 additions & 0 deletions chain/waves/client_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package waves

import (
"context"

"github.com/renproject/pack"
)

type Client interface {
// Tx returns the transaction uniquely identified by the given transaction
// hash. It also returns the number of confirmations for the transaction. If
// the transaction cannot be found before the context is done, or the
// transaction is invalid, then an error should be returned.
Tx(ctx context.Context, id pack.Bytes) (Tx, pack.U64, error)

// SubmitTx to the underlying chain. If the transaction cannot be found
// before the context is done, or the transaction is invalid, then an error
// should be returned.
SubmitTx(context.Context, Tx) error
}
71 changes: 71 additions & 0 deletions chain/waves/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package waves

import (
"github.com/pkg/errors"
"github.com/renproject/multichain/api/address"
"github.com/renproject/multichain/api/contract"
"github.com/renproject/pack"
"github.com/wavesplatform/gowaves/pkg/crypto"
"github.com/wavesplatform/gowaves/pkg/proto"
)

// Implements Tx interface in tx_interface.go.
type TxImpl struct {
tx *proto.TransferWithProofs
chainID proto.Scheme
}

func (a TxImpl) OriginalTx() *proto.TransferWithProofs {
return a.tx
}

func newTx(t *proto.TransferWithProofs, chainID proto.Scheme) (*TxImpl, error) {
if t.Recipient.Address == nil {
if t.Recipient.Alias != nil {
return nil, errors.New("unsupported transaction with alias")
}
return nil, errors.New("unsupported transaction with empty recipient")
}
return &TxImpl{
tx: t,
chainID: chainID,
}, nil
}

func (a TxImpl) Hash() pack.Bytes {
id, _ := a.tx.GetID(a.chainID)
return id
}

func (a TxImpl) From() PublicKey {
return a.tx.SenderPK.String()
}

func (a TxImpl) To() address.Address {
return address.Address(a.tx.Recipient.Address.String())
}

func (a TxImpl) Value() pack.U256 {
return pack.NewU256FromU64(pack.NewU64(a.tx.Amount))
}

func (a TxImpl) Nonce() pack.U256 {
return pack.NewU256FromU64(pack.NewU64(a.tx.Timestamp))
}

// Looks like this method is useless.
func (a TxImpl) Payload() contract.CallData {
return contract.CallData{}
}

func (a TxImpl) Sign(privateKey pack.Bytes) error {
secret, err := crypto.NewSecretKeyFromBytes(privateKey)
if err != nil {
return err
}
return a.tx.Sign(a.chainID, secret)
}

func (a TxImpl) Serialize() (pack.Bytes, error) {
return a.tx.MarshalSignedToProtobuf(a.chainID)
}
24 changes: 24 additions & 0 deletions chain/waves/tx_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package waves

import (
"github.com/renproject/multichain/api/address"
"github.com/renproject/multichain/api/contract"
"github.com/renproject/pack"
"github.com/wavesplatform/gowaves/pkg/proto"
)

type Tx interface {
Hash() pack.Bytes
From() PublicKey
To() address.Address
Value() pack.U256
Nonce() pack.U256
Payload() contract.CallData
Sign(privateKey pack.Bytes) error
Serialize() (pack.Bytes, error)
}

// Way to get waves transaction from Tx interface.
type OriginalTx interface {
OriginalTx() *proto.TransferWithProofs
}
1 change: 1 addition & 0 deletions chain/waves/waves.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package waves
13 changes: 13 additions & 0 deletions chain/waves/waves_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package waves_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestWaves(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Waves Suite")
}
Loading