Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make anyone can register block syncer code to the Glueby::BlockSyncer #66

Merged
merged 10 commits into from Jun 30, 2021
123 changes: 113 additions & 10 deletions README.md
@@ -1,9 +1,31 @@
# Glueby [![Ruby](https://github.com/chaintope/glueby/actions/workflows/ruby.yml/badge.svg)](https://github.com/chaintope/glueby/actions/workflows/ruby.yml) [![Gem Version](https://badge.fury.io/rb/glueby.svg)](https://badge.fury.io/rb/glueby) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)

Glueby is a smart contract library on the [Tapyrus blockchain](https://github.com/chaintope/tapyrus-core). This is
designed as you can use without any deep blockchain understanding.

Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/glueby`. To experiment with that code, run `bin/console` for an interactive prompt.
## Features

TODO: Delete this and the text above, and describe your gem
Glueby has below features.

1. Wallet
You can manage wallets for application users. This wallet feature is a foundation of Contracts below to specify tx
sender and so on.
You can choose two sorts of wallet implementation :activerecord and :core. :activerecord is implemented using
ActiveRecord on RDB. :core uses the wallet that bundled with Tapyrus Core.

2. Contracts
You can use some smart contracts easily
- [Timestamp](#Timestamp): Record any data as a timestamp to a tapyrus blockchain.
- [Payment](./lib/glueby/contract/payment.rb): Transfer TPC.
- [Token](./lib/glueby/contract/token.rb): Issue, transfer and burn colored coin.

3. Sync blocks with your application
You can use BlockSyncer when you need to synchronize the state of an application with the state of a blockchain.
See more details at [BlockSyncer](./lib/glueby/block_syncer.rb).

4. Take over tx sender's fees
FeeProvider module can bear payments of sender's fees. You should provide funds for fees to FeeProvider before use.
See how to set up at [Use fee provider mode](#use-fee-provider-mode)

## Installation

Expand All @@ -21,13 +43,95 @@ Or install it yourself as:

$ gem install glueby

## Usage
### Setup for Ruby on Rails application development

Glueby has below features.
1. Add this line to your application's Gemfile

```ruby
gem 'glueby'
```

and then execute

$ bundle install

2. Run installation rake task

$ rails glueby:contract:install

3. Run Tapyrus Core as dev mode

We recommend to run as a Docker container.
Docker image is here.

* [tapyus/tapyrusd](https://hub.docker.com/repository/docker/tapyrus/tapyrusd)

Starts tapryusd container

$ docker run -d --name 'tapyrus_node_dev' -p 12381:12381 -e GENESIS_BLOCK_WITH_SIG='0100000000000000000000000000000000000000000000000000000000000000000000002b5331139c6bc8646bb4e5737c51378133f70b9712b75548cb3c05f9188670e7440d295e7300c5640730c4634402a3e66fb5d921f76b48d8972a484cc0361e66ef74f45e012103af80b90d25145da28c583359beb47b21796b2fe1a23c1511e443e7a64dfdb27d40e05f064662d6b9acf65ae416379d82e11a9b78cdeb3a316d1057cd2780e3727f70a61f901d10acbe349cd11e04aa6b4351e782c44670aefbe138e99a5ce75ace01010000000100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100f2052a010000001976a91445d405b9ed450fec89044f9b7a99a4ef6fe2cd3f88ac00000000' tapyrus/tapyrusd:edge

4. Modify the glueby configuration

```ruby
# Use tapyrus dev network
Tapyrus.chain_params = :dev
Glueby.configure do |config|
config.wallet_adapter = :activerecord
# Modify rpc connection info in config/initializers/glueby.rb that is created in step 3.
config.rpc_config = { schema: 'http', host: '127.0.0.1', port: 12381, user: 'rpcuser', password: 'rpcpassword' }
end
```

5. Generate db migration files for wallet feature

These are essential if you use `config.wallet_adapter = :activerecord` configuration.

$ rails g glueby:contract:block_syncer
$ rails g glueby:contract:wallet_adapter

If you want to use reissuable token or timestamp, you need to do below generators.

$ rails g glueby:contract:reissuable_token
$ rails g glueby:contract:timestamp

Then, run the migrations.

$ rails db:migrate

### Provide initial TPC (Tapyrus Coin) to wallets

To use contracts, wallets need to have TPC and it can be provided from coinbase tx.

1. Create a wallet and get receive address

```ruby
wallet = Glueby::Wallet.create
wallet.balances # => {}
address = wallet.internal_wallet.receive_address
puts address
```

2. Generate a block

Set an address you got in previous step to `[Your address]`

$ docker exec tapyrus_node_dev tapyrus-cli -conf=/etc/tapyrus/tapyrus.conf generatetoaddress 1 "[Your address]" "cUJN5RVzYWFoeY8rUztd47jzXCu1p57Ay8V7pqCzsBD3PEXN7Dd4"

3. Sync blocks if you use `:activerecord` wallet adapter

You don't need to do this if you are using `:core` wallet_adapter.

$ rails glueby:contract:block_syncer:start

Here the wallet created in step 1 have 50 TPC and you can see like this:

```ruby
wallet.balances # => {""=>5000000000}
```

- [Timestamp](#Timestamp)
TPC amount is shown as tapyrus unit. 1 TPC = 100000000 tapyrus.

### Timestamp
## Timestamp

```ruby

Expand Down Expand Up @@ -99,7 +203,7 @@ We can see the timestamp transaction using getrawblockchain command
}
```

#### Rails support
### Rails support

Glueby supports ruby on rails integration.

Expand Down Expand Up @@ -155,11 +259,10 @@ bin/rails glueby:contract:timestamp:create
broadcasted (id=1, txid=8d602ca8ebdd50fa70b5ee6bc6351965b614d0a4843adacf9f43fedd7112fbf4)
```

Run `glueby:contract:timestamp:confirm` task to confirm the transaction and update status(unconfirmed -> confirmded).
Run `glueby:block_syncer:start` task to confirm the transaction and update status(unconfirmed -> confirmded).

```
bin/rails glueby:contract:timestamp:confirm
confirmed (id=1, txid=8d602ca8ebdd50fa70b5ee6bc6351965b614d0a4843adacf9f43fedd7112fbf4)
bin/rails glueby:block_syncer:start
```

## Use fee provider mode
Expand Down
6 changes: 4 additions & 2 deletions lib/generators/glueby/contract/templates/initializer.rb.erb
@@ -1,3 +1,5 @@
# Edit configuration for connection to tapyrus core
config = {adapter: 'core', schema: 'http', host: '127.0.0.1', port: 12381, user: 'user', password: 'pass'}
Glueby::Wallet.configure(config)
Glueby.configure do |config|
config.wallet_adapter = :activerecord
config.rpc_config = { schema: 'http', host: '127.0.0.1', port: 12381, user: 'user', password: 'pass' }
end
20 changes: 5 additions & 15 deletions lib/glueby.rb
Expand Up @@ -9,27 +9,17 @@ module Glueby
autoload :AR, 'glueby/active_record'
autoload :FeeProvider, 'glueby/fee_provider'
autoload :Configuration, 'glueby/configuration'
autoload :BlockSyncer, 'glueby/block_syncer'

if defined? ::Rails::Railtie
require 'glueby/railtie'
end

# Add prefix to activerecord table names
def self.table_name_prefix
'glueby_'
end

begin
class Railtie < ::Rails::Railtie
rake_tasks do
load "tasks/glueby/contract.rake"
load "tasks/glueby/contract/timestamp.rake"
load "tasks/glueby/contract/wallet_adapter.rake"
load "tasks/glueby/contract/block_syncer.rake"
load "tasks/glueby/fee_provider.rake"
end
end
rescue
# Rake task is unavailable
puts "Rake task is unavailable"
end

# Returns the global [Configuration](RSpec/Core/Configuration) object.
def self.configuration
@configuration ||= Glueby::Configuration.new
Expand Down
98 changes: 98 additions & 0 deletions lib/glueby/block_syncer.rb
@@ -0,0 +1,98 @@
module Glueby
# You can use BlockSyncer when you need to synchronize the state of
# an application with the state of a blockchain. When BlockSyncer
# detects the generation of a new block, it executes the registered
# syncer code on a block-by-block or transaction-by-transaction basis.
# By using this, an application can detect that the issued transaction
# has been captured in blocks, receive a new remittance, and so on.
#
# # Syncer logic registration
#
# For registration, create a class that implements the method that performs
# synchronization processing and registers it in BlockSyncer. Implement
# methods with the following name in that class.
#
# Method name | Arguments | Call conditions
# ------------------ | --------------------- | ------------------------------
# block_sync (block) | block: Tapyrus::Block | When a new block is created
# block_tx (tx) | tx: Tapyrus::Tx | When a new block is created, it is executed for each tx contained in that block.
#
# @example Register a synchronous logic
# class Syncer
# def block_sync (block)
# # sync a block
# end
#
# def tx_sync (tx)
# # sync a tx
# end
# end
# BlockSyncer.register_syncer(Syncer)
#
# @example Unregister the synchronous logic
# BlockSyncer.unregister_syncer(Syncer)
#
# # Run BlockSyncer
#
# Run the `glueby: block_syncer: start` rake task periodically with a program
# for periodic execution such as cron. If it detects the generation of a new
# block when it is executed, the synchronization process will be executed.
# Determine the execution interval according to the requirements of the application.
class BlockSyncer
# @!attribute [r] height
# @return [Integer] The block height to be synced
attr_reader :height

class << self
# @!attribute r syncers
# @return [Array<Class>] The syncer classes that is registered
attr_reader :syncers

# Register syncer class
# @param [Class] syncer The syncer to be registered.
def register_syncer(syncer)
@syncers ||= []
@syncers << syncer
end

# Unregister syncer class
# @param [Class] syncer The syncer to be unregistered.
def unregister_syncer(syncer)
@syncers ||= []
@syncers.delete(syncer)
end
end

# @param [Integer] height The block height to be synced in the instance
def initialize(height)
@height = height
end

# Run a block synchronization
def run
return if self.class.syncers.nil?

self.class.syncers.each do |syncer|
instance = syncer.new
instance.block_sync(block) if instance.respond_to?(:block_sync)

if instance.respond_to?(:tx_sync)
block.transactions.each { |tx| instance.tx_sync(tx) }
end
end
end

private

def block
@block ||= begin
block = Glueby::Internal::RPC.client.getblock(block_hash, 0)
Tapyrus::Block.parse_from_payload(block.htb)
end
end

def block_hash
@block_hash ||= Glueby::Internal::RPC.client.getblockhash(height)
end
end
end
2 changes: 2 additions & 0 deletions lib/glueby/contract/timestamp.rb
Expand Up @@ -10,6 +10,8 @@ module Contract
class Timestamp
include Glueby::Contract::TxBuilder

autoload :Syncer, 'glueby/contract/timestamp/syncer'

module Util
include Glueby::Internal::Wallet::TapyrusCoreWalletAdapter::Util
module_function
Expand Down
13 changes: 13 additions & 0 deletions lib/glueby/contract/timestamp/syncer.rb
@@ -0,0 +1,13 @@
module Glueby
module Contract
class Timestamp
class Syncer
def block_sync(block)
Glueby::Contract::AR::Timestamp
.where(txid: block.transactions.map(&:txid), status: :unconfirmed)
.update_all(status: :confirmed)
end
end
end
end
end
18 changes: 15 additions & 3 deletions lib/glueby/internal/wallet.rb
Expand Up @@ -34,8 +34,6 @@ class Wallet
autoload :Errors, 'glueby/internal/wallet/errors'

class << self
attr_writer :wallet_adapter

def create(wallet_id = nil)
begin
wallet_id = wallet_adapter.create_wallet(wallet_id)
Expand All @@ -58,6 +56,16 @@ def wallets
wallet_adapter.wallets.map { |id| new(id) }
end

def wallet_adapter=(adapter)
if adapter.is_a?(ActiveRecordWalletAdapter)
BlockSyncer.register_syncer(ActiveRecordWalletAdapter::Syncer)
else
BlockSyncer.unregister_syncer(ActiveRecordWalletAdapter::Syncer)
end

@wallet_adapter = adapter
end

def wallet_adapter
@wallet_adapter or
raise Errors::ShouldInitializeWalletAdapter, 'You should initialize wallet adapter using `Glueby::Internal::Wallet.wallet_adapter = some wallet adapter instance`.'
Expand Down Expand Up @@ -102,8 +110,12 @@ def sign_tx(tx, prev_txs = [], for_fee_provider_input: false)
# Broadcast a transaction via Tapyrus Core RPC
# @param [Tapyrus::Tx] tx The tx that would be broadcasted
# @option [Boolean] without_fee_provider The flag to avoid to use FeeProvider temporary.
def broadcast(tx, without_fee_provider: false)
# @param [Proc] block The block that is called before broadcasting. It can be used to handle tx that is modified by FeeProvider.
def broadcast(tx, without_fee_provider: false, &block)
tx = FeeProvider.provide(tx) if !without_fee_provider && Glueby.configuration.fee_provider_bears?

block.call(tx) if block

wallet_adapter.broadcast(id, tx)
tx
end
Expand Down
3 changes: 3 additions & 0 deletions lib/glueby/internal/wallet/active_record_wallet_adapter.rb
Expand Up @@ -54,6 +54,9 @@ class Wallet
# alice_wallet.balances
# ```
class ActiveRecordWalletAdapter < AbstractWalletAdapter

autoload :Syncer, 'glueby/internal/wallet/active_record_wallet_adapter/syncer'

def create_wallet(wallet_id = nil)
wallet_id = SecureRandom.hex(16) unless wallet_id
begin
Expand Down
14 changes: 14 additions & 0 deletions lib/glueby/internal/wallet/active_record_wallet_adapter/syncer.rb
@@ -0,0 +1,14 @@
module Glueby
module Internal
class Wallet
class ActiveRecordWalletAdapter
class Syncer
def tx_sync(tx)
Glueby::Internal::Wallet::AR::Utxo.destroy_for_inputs(tx)
Glueby::Internal::Wallet::AR::Utxo.create_or_update_for_outputs(tx)
end
end
end
end
end
end