Skip to content

Commit 2a686a3

Browse files
Add transaction representation
1 parent 0fdcafc commit 2a686a3

File tree

7 files changed

+142
-3
lines changed

7 files changed

+142
-3
lines changed

app/entity/block.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ class Entity::Block < Entity::Base
44
attribute :index, Types::Integer
55
attribute :previous_hash, Types::String
66
attribute :timestamp, Types::Integer
7-
8-
attribute :data, Types::Array
9-
107
attribute :nonce, Types::Integer.default(0)
118
attribute :block_hash, Types::String.optional.default(nil)
129

10+
attribute :data, Types::Array
11+
1312
def self.build_genesis(difficulty)
1413
new(index: 0, previous_hash: "0" * 64, timestamp: Time.now.utc.to_i, data: ["Initial block"]).mine!(difficulty)
1514
end

app/entity/transaction.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
class Entity::Transaction < Entity::Base
4+
attribute :id, Types::String.optional.default(nil)
5+
attribute :inputs, Types::Array.of(Types.Instance(Value::Transaction::Input)).constrained(min_size: 1)
6+
attribute :outputs, Types::Array.of(Types.Instance(Value::Transaction::Output)).constrained(min_size: 1)
7+
8+
def self.build(inputs, outputs)
9+
new(inputs:, outputs:).calculate_id!
10+
end
11+
12+
def calculate_id!
13+
self.id ||= Digest::SHA256.hexdigest([inputs, outputs].flatten.join("|"))
14+
self
15+
end
16+
17+
alias to_s id
18+
end

app/value/base.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
class Value::Base < Dry::Struct
4+
transform_keys(&:to_sym)
5+
end

app/value/transaction/input.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
class Value::Transaction::Input < Value::Base
4+
attribute :transaction_id, Types::String
5+
attribute :output_index, Types::Integer
6+
7+
def to_s
8+
"#{transaction_id}:#{output_index}"
9+
end
10+
end

app/value/transaction/output.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
class Value::Transaction::Output < Value::Base
4+
attribute :address, Types::String
5+
attribute :amount, Types::Decimal.constrained(gteq: BigDecimal("0"))
6+
7+
def to_s
8+
"#{address}:#{amount}"
9+
end
10+
end

config.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
# Standard libraries
4+
require "bigdecimal"
45
require "digest"
56
require "time"
67

spec/entity/transaction_spec.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
describe Entity::Transaction do
4+
subject(:transaction) { described_class.build(inputs, outputs) }
5+
6+
let(:inputs) { [input] }
7+
let(:input) { Value::Transaction::Input.new(transaction_id: "0" * 64, output_index: 0) }
8+
let(:outputs) { [output] }
9+
let(:output) { Value::Transaction::Output.new(address: "some_wallet_address", amount: BigDecimal("0.1")) }
10+
11+
context "when new transaction are built" do
12+
let(:expected_transaction_id) { "809d7b433611e9614857f6647132b04db9478810840416ba5c9ac5db91628b00" }
13+
14+
it "creates a new transaction with the correct id, inputs and outputs" do
15+
expect(transaction.id).to eq(expected_transaction_id)
16+
17+
expect(transaction.inputs.size).to eq(1)
18+
expect(transaction.inputs.first).to be_a(Value::Transaction::Input)
19+
expect(transaction.inputs.first.transaction_id).to eq("0" * 64)
20+
expect(transaction.inputs.first.output_index).to eq(0)
21+
22+
expect(transaction.outputs.size).to eq(1)
23+
expect(transaction.outputs.first).to be_a(Value::Transaction::Output)
24+
expect(transaction.outputs.first.address).to eq("some_wallet_address")
25+
expect(transaction.outputs.first.amount).to eq(BigDecimal("0.1"))
26+
end
27+
end
28+
29+
context "with multiple inputs and outputs" do
30+
let(:expected_transaction_id) { "e4fa40425e82357e636f0e74937c7338fade9adade22a65d92d68f2eb37787b7" }
31+
32+
let(:inputs) do
33+
[
34+
Value::Transaction::Input.new(transaction_id: "0" * 64, output_index: 0),
35+
Value::Transaction::Input.new(transaction_id: "1" * 64, output_index: 1),
36+
]
37+
end
38+
let(:outputs) do
39+
[
40+
Value::Transaction::Output.new(address: "address1", amount: BigDecimal("0.5")),
41+
Value::Transaction::Output.new(address: "address2", amount: BigDecimal("0.3")),
42+
]
43+
end
44+
45+
it "creates transaction with multiple inputs and outputs" do
46+
expect(transaction.id).to eq(expected_transaction_id)
47+
48+
expect(transaction.inputs.size).to eq(2)
49+
expect(transaction.outputs.size).to eq(2)
50+
expect(transaction.inputs.map(&:transaction_id)).to eq(["0" * 64, "1" * 64])
51+
expect(transaction.outputs.map(&:address)).to eq(%w[address1 address2])
52+
end
53+
end
54+
55+
context "with empty inputs" do
56+
let(:inputs) { [] }
57+
let(:part_of_error_message) { "has invalid type for :inputs violates constraints" }
58+
59+
it "raises an error" do
60+
expect { transaction }.to raise_error(Dry::Struct::Error, a_string_including(part_of_error_message))
61+
end
62+
end
63+
64+
context "with empty outputs" do
65+
let(:outputs) { [] }
66+
let(:part_of_error_message) { "has invalid type for :outputs violates constraints" }
67+
68+
it "raises an error" do
69+
expect { transaction }.to raise_error(Dry::Struct::Error, a_string_including(part_of_error_message))
70+
end
71+
end
72+
73+
context "with invalid input type" do
74+
let(:inputs) { ["invalid"] }
75+
76+
it "raises a type error" do
77+
expect { transaction }.to raise_error(Dry::Struct::Error)
78+
end
79+
end
80+
81+
context "with invalid output type" do
82+
let(:outputs) { ["invalid"] }
83+
84+
it "raises a type error" do
85+
expect { transaction }.to raise_error(Dry::Struct::Error)
86+
end
87+
end
88+
89+
context "with negative amount" do
90+
let(:output) { Value::Transaction::Output.new(address: "address", amount: BigDecimal("-0.1")) }
91+
92+
it "raises an error for negative amount" do
93+
expect { transaction }.to raise_error(Dry::Struct::Error)
94+
end
95+
end
96+
end

0 commit comments

Comments
 (0)