ordinals 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24a92228c4c130bdb9083c1fa9dea7d458d827d99533935191fe5f31c4c750c8
4
- data.tar.gz: 1807eb1c8472696cff5c46ef41b8e7126517e56a7f458e352e5aa39aaa537d65
3
+ metadata.gz: 23ab1c1b894f3d7d528d90943d890e07c40d878cc56b54206384be335ae47fa6
4
+ data.tar.gz: f3ca9276b04d13052424d9a4834204d7153a92681507efac9c8511949e6ec362
5
5
  SHA512:
6
- metadata.gz: 1b40c581901fe69993d189bcd520a9c6d47cb90a7d1e5512764cc79842e21f3651b8862f3b2de62ca131ca721eaf73745721a775e80b7ec0b064e10da530bc7e
7
- data.tar.gz: '036823aabca7d9c3d5deb3dfa404a7d4bfe22caecec84ff21f61fac10f44ae3f18217bbc0f80cb269aad35e6e61c74b375de3615bebe0b02e97482f0ccecabaa'
6
+ metadata.gz: a91a05ebbaf2ab66030ef45bf9a8da6744df0f40d3014451364f1d802a0193c0a7450f90647612e95c6469b7c21a6f8c6f04a10e3a354d820cafa88de32ccc6b
7
+ data.tar.gz: 30ca5d81426d13f3149a37f9eb8a7bc3a703c22d9cf0685c991033273ae81efe4917fce3d00f68aadfaf9742ab0d626bdc6e290a2f52c8b2c967b0453ebaa475
data/Manifest.txt CHANGED
@@ -2,4 +2,9 @@ CHANGELOG.md
2
2
  Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
+ bin/ordbase
5
6
  lib/ordinals.rb
7
+ lib/ordinals/api.rb
8
+ lib/ordinals/collection.rb
9
+ lib/ordinals/stats.rb
10
+ lib/ordinals/tool.rb
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ordinals
2
2
 
3
- ordinals gem - (off-chain) ordinals (pixel art) machinery & helpers for Bitcoin & co.
3
+ ordinals gem - "right-clicker" (off-chain) ordinals (pixel art) machinery & helpers for Bitcoin, Litcoin, Dogecoin & co.
4
4
 
5
5
 
6
6
  * home :: [github.com/pixelartexchange/ordinals.sandbox](https://github.com/pixelartexchange/ordinals.sandbox)
@@ -9,9 +9,152 @@ ordinals gem - (off-chain) ordinals (pixel art) machinery & helpers for Bitcoin
9
9
  * rdoc :: [rubydoc.info/gems/ordinals](http://rubydoc.info/gems/ordinals)
10
10
 
11
11
 
12
- ## Usage
13
12
 
14
- To be done
13
+ ## Command-Line Usage
14
+
15
+ Let's use the 100 Ordinal Punks collection to try out the
16
+ `ordbase` command-line tool shipping with the ordinals package.
17
+
18
+ Tip: New to Ordinal Punks? For some background see [**Awesome 100 Ordinal Punks (Anno 2023) Notes - 24×24 Pixel Art on the (Bitcoin) Blockchain »**](https://github.com/cryptopunksnotdead/cryptopunks/tree/master/awesome-ordinalpunks)
19
+
20
+
21
+ ### Step 0: Prepare A Tabular Dataset (List) Of All Ordinals w/ ID
22
+
23
+ For now a manual step - prepare a list of all ordinals with id in the comma-separated values (.csv) tabular dataset format.
24
+ Example - [ordinalpunks/ordinals.csv](https://github.com/pixelartexchange/ordinals.sandbox/blob/master/ordinalpunks/ordinals.csv):
25
+
26
+ ``` csv
27
+ num, id
28
+ 1, 96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0
29
+ 2, acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37i0
30
+ 3, 0406654dffdd01a49794bd8531bf33721986cc7c6546f871962adee921a39a9di0
31
+ 4, 2fe9bb034f60db694701acb23a76c3d7d5aba4328dbd315764f6ee406ba41786i0
32
+ 5, dcfa240f2681d1e4a8948120a3a64567262e3c78d5497cb4e97351bfa836b638i0
33
+ 6, 16df62c86321895df2b93236d103c935015ed77e189485be649ce2c7e6ac8a4ei0
34
+ 7, 81e8d9159b8e9a27c692a5bb3ba18ca037757e94e975b53e175eaaeb2c52f15ai0
35
+ 8, c2e15fe87c4b1fd61de65f2804858e6d1152b6316bcb9c2b39b69c9c21638f5di0
36
+ 9, 3ed569f3a92ade9f1b47031eb2db2045e7dee3e00787954a88c67ed2ad9854bbi0
37
+ ...
38
+ ```
39
+
40
+
41
+ ### Step 1: Download All Pixel Art Images Via Ordinals.com
42
+
43
+ Use
44
+
45
+ ```
46
+ $ ordbase ordinalpunks image # or
47
+ $ ordbase ordinalpunks img
48
+ ```
49
+
50
+ to download all images via the ordinals.com (web) service.
51
+ All images get stored in the (temporary) `token-i/` directory.
52
+ Resulting in:
53
+
54
+ ```
55
+ /ordinalpunks
56
+ ordinals.csv
57
+ /token-i
58
+ 1.png
59
+ 2.png
60
+ 3.png
61
+ ...
62
+ ```
63
+
64
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinals/i/1.png)
65
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinals/i/2.png)
66
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinals/i/3.png)
67
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinals/i/4.png)
68
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinals/i/5.png)
69
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinals/i/6.png)
70
+ ...
71
+
72
+
73
+
74
+
75
+ ### Step 2: Downsample ("Pixelate") All Pixel Art Images
76
+
77
+ Note: Most pixel art collections upload / inscribe images with a zoom.
78
+ The ordinal punks, for example, use a 8x zoom factor for the 24×24px originals, thus,
79
+ resulting in 192×192px.
80
+
81
+
82
+ Add a ["artbase-compatible"](https://github.com/pixelartexchange/artbase) collection configuration file to lists the source format(s)
83
+ and the minimal true pixel format.
84
+ Example - [ordinalpunks/collection.yml](https://github.com/pixelartexchange/ordinals.sandbox/blob/master/ordinalpunks/collection.yml):
85
+
86
+ ``` yaml
87
+ slug: ordinalpunks
88
+ count: 100
89
+ format: 24x24
90
+ source: 192x192
91
+ offset: 1
92
+ ```
93
+
94
+ Use
95
+
96
+ ```
97
+ $ ordbase ordinalpunks pixelate # or
98
+ $ ordbase ordinalpunks px
99
+ ```
100
+
101
+ to downsample ("pixelate") all images
102
+ in the (temporary) `token-i/` directory.
103
+ Resulting in a `24x24`/ directory with all images
104
+ in the "minimal" `24x24` format:
105
+
106
+ ```
107
+ /ordinalpunks
108
+ ordinals.csv
109
+ collections.yml
110
+ /24x24
111
+ 1.png
112
+ 2.png
113
+ 3.png
114
+ ...
115
+ ```
116
+
117
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinalpunks/24x24/1.png)
118
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinalpunks/24x24/2.png)
119
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinalpunks/24x24/3.png)
120
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinalpunks/24x24/4.png)
121
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinalpunks/24x24/5.png)
122
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/ordinalpunks/24x24/6.png)
123
+ ...
124
+
125
+
126
+
127
+ ### Bonus: Step 3: Make An All-In-One Collect'Em All Composite Image
128
+
129
+
130
+ Use
131
+
132
+ ```
133
+ $ ordbase ordinalpunks composite # or
134
+ $ ordbase ordinalpunks comp
135
+ ```
136
+
137
+ to make an all-in-one image composite for the complete collection.
138
+ Resulting in `/tmp/ordinalpunks.png` (~11kb).
139
+
140
+
141
+ ![](https://github.com/pixelartexchange/ordinals.sandbox/raw/master/i/ordinalpunks.png)
142
+
143
+
144
+ That's it for now.
145
+
146
+
147
+
148
+
149
+ ## Bonus: More Ordinal Pixel Art Collections
150
+
151
+
152
+ See the [**Ordinals (Pixel Art) Sandbox (& Cache)**](https://github.com/pixelartexchange/ordinals.sandbox)
153
+ for more collections incl. Bitcoin Punks (24×24), Ordinal Mini Doges (24×24),
154
+ Extra Ordinal Women (32×32), Ordinal Penguins (35×35),
155
+ Ordinal Birds (42×42), Bitcoin Bears (48×48) and much more.
156
+
157
+ Add your sandbox or "right-clicker" ordinal backup / archive / gallery here. Yes, you can.
15
158
 
16
159
 
17
160
 
@@ -23,6 +166,6 @@ Use it as you please with no restrictions whatsoever.
23
166
 
24
167
  ## Questions? Comments?
25
168
 
169
+ Post them over at the [Help & Support](https://github.com/geraldb/help) page. Thanks.
26
170
 
27
- Post them on the [D.I.Y. Punk (Pixel) Art reddit](https://old.reddit.com/r/DIYPunkArt). Thanks.
28
171
 
data/Rakefile CHANGED
@@ -10,9 +10,9 @@ end
10
10
 
11
11
  Hoe.spec 'ordinals' do
12
12
 
13
- self.version = '0.0.1'
13
+ self.version = '1.0.0'
14
14
 
15
- self.summary = "ordinals gem - (off-chain) ordinals (pixel art) machinery & helpers for Bitcoin & co."
15
+ self.summary = 'ordinals gem - "right-clicker" (off-chain) ordinals (pixel art) machinery & helpers for Bitcoin, Litecoin, Dogecoin & co.'
16
16
  self.description = summary
17
17
 
18
18
  self.urls = { home: 'https://github.com/pixelartexchange/ordinals.sandbox' }
@@ -27,6 +27,7 @@ Hoe.spec 'ordinals' do
27
27
  self.extra_deps = [
28
28
  ['cocos'],
29
29
  ['pixelart'],
30
+ ['nokogiri'], ## required / used by api support (html parsing)
30
31
  ]
31
32
 
32
33
  self.licenses = ['Public Domain']
data/bin/ordbase ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ###################
4
+ # == DEV TIPS:
5
+ #
6
+ # For local testing run like:
7
+ #
8
+ # ruby -Ilib bin/ordbase
9
+ #
10
+ # Set the executable bit in Linux. Example:
11
+ #
12
+ # % chmod a+x bin/ordbase
13
+ #
14
+
15
+ require 'ordinals'
16
+
17
+ Ordinals::Tool.main
@@ -0,0 +1,224 @@
1
+
2
+
3
+ module Ordinals
4
+
5
+
6
+ class Api ## change/rename Api to Client - why? why not?
7
+ def self.litecoin
8
+ @litecoin ||= new( 'https://litecoin.earlyordies.com' )
9
+ @litecoin
10
+ end
11
+
12
+ def self.bitcoin
13
+ @bitcoin ||= new( 'https://ordinals.com' )
14
+ @bitcoin
15
+ end
16
+ ## todo: add ltc and btc alias - why? why not?
17
+
18
+ def self.dogecoin
19
+ ## note: "doginals" call inscriptions
20
+ ## shibescriptions
21
+ @dogecoin ||= new( 'https://doginals.com', inscription: 'shibescription' )
22
+ @dogecoin
23
+ end
24
+
25
+
26
+ def initialize( base, inscription: 'inscription' )
27
+ @base = base
28
+ @inscription = inscription
29
+ end
30
+
31
+
32
+ ###
33
+ # use a struct-like content class - why? why not?
34
+ class Content
35
+ attr_reader :data,
36
+ :type,
37
+ :length
38
+ def initialize( data, type, length )
39
+ @data = data
40
+ @type = type
41
+ @length = length
42
+ end
43
+
44
+ alias_method :blob, :data
45
+ end ## (nested) class Content
46
+
47
+
48
+
49
+ def content( id )
50
+ src = "#{@base}/content/#{id}"
51
+ res = get( src )
52
+
53
+ content_type = res.content_type
54
+ content_length = res.content_length
55
+
56
+ ## note - content_length -- returns an integer (number)
57
+ ## puts "content_length:"
58
+ ## print content_length.inspect
59
+ ## print " - #{content_length.class.name}\n"
60
+
61
+ content = Content.new(
62
+ res.blob,
63
+ content_type,
64
+ content_length )
65
+ content
66
+ end
67
+
68
+
69
+ =begin
70
+ <dl>
71
+ <dt>id</dt>
72
+ <dd class=monospace>d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2i0</dd>
73
+ <dt>address</dt>
74
+ <dd class=monospace>bc1pqapcyswesvccgqsmuncd96ylghs9juthqeshdr8smmh9w7azn8zsghjjar</dd>
75
+ <dt>output value</dt>
76
+ <dd>10000</dd>
77
+ <dt>sat</dt>
78
+ <dd><a href=/sat/1320953397332258>1320953397332258</a></dd>
79
+ <dt>preview</dt>
80
+ <dd><a href=/preview/d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2i0>link</a></dd>
81
+ <dt>content</dt>
82
+ <dd><a href=/content/d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2i0>link</a></dd>
83
+ <dt>content length</dt>
84
+ <dd>71997 bytes</dd>
85
+ <dt>content type</dt>
86
+ <dd>text/plain;charset=utf-8</dd>
87
+ <dt>timestamp</dt>
88
+ <dd><time>2023-02-11 21:39:00 UTC</time></dd>
89
+ <dt>genesis height</dt>
90
+ <dd><a href=/block/776090>776090</a></dd>
91
+ <dt>genesis fee</dt>
92
+ <dd>273630</dd>
93
+ <dt>genesis transaction</dt>
94
+ <dd><a class=monospace href=/tx/d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2>d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2</a></dd>
95
+ <dt>location</dt>
96
+ <dd class=monospace>d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2:0:0</dd>
97
+ <dt>output</dt>
98
+ <dd><a class=monospace href=/output/d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2:0>d026ac5994f698dba475681359b6c29d6d39a895484b95e06b7ae49921d80df2:0</a></dd>
99
+ <dt>offset</dt>
100
+ <dd>0</dd>
101
+ </dl>
102
+
103
+ <dl>
104
+ <dt>id</dt>
105
+ <dd class=monospace>acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37i0</dd>
106
+ <dt>address</dt>
107
+ <dd class=monospace>bc1qx3scwushtwenxtxlnjet4x2w5vg35etdtse5u0</dd>
108
+ <dt>output value</dt>
109
+ <dd>8020</dd>
110
+ <dt>sat</dt>
111
+ <dd><a href=/sat/1883186433806857>1883186433806857</a></dd>
112
+ <dt>preview</dt>
113
+ <dd><a href=/preview/acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37i0>link</a></dd>
114
+ <dt>content</dt>
115
+ <dd><a href=/content/acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37i0>link</a></dd>
116
+ <dt>content length</dt>
117
+ <dd>529 bytes</dd>
118
+ <dt>content type</dt>
119
+ <dd>image/png</dd>
120
+ <dt>timestamp</dt>
121
+ <dd><time>2023-01-31 19:34:47 UTC</time></dd>
122
+ <dt>genesis height</dt>
123
+ <dd><a href=/block/774489>774489</a></dd>
124
+ <dt>genesis fee</dt>
125
+ <dd>5340</dd>
126
+ <dt>genesis transaction</dt>
127
+ <dd><a class=monospace href=/tx/acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37>acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37</a></dd>
128
+ <dt>location</dt>
129
+ <dd class=monospace>6630ff2153985504b180fc16721d559a4cecdc66f4be6acf33509ec2100c0aa5:0:0</dd>
130
+ <dt>output</dt>
131
+ <dd><a class=monospace href=/output/6630ff2153985504b180fc16721d559a4cecdc66f4be6acf33509ec2100c0aa5:0>6630ff2153985504b180fc16721d559a4cecdc66f4be6acf33509ec2100c0aa5:0</a></dd>
132
+ <dt>offset</dt>
133
+ <dd>0</dd>
134
+ </dl>
135
+
136
+
137
+
138
+ <title>Inscription 407</title>
139
+
140
+ id =
141
+ genesis transaction + offset ???
142
+
143
+ genesis transaction: acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37
144
+ id: acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37i0
145
+ address: bc1qx3scwushtwenxtxlnjet4x2w5vg35etdtse5u0
146
+ output value: 8020
147
+ sat: 1883186433806857
148
+ content length: 529 bytes
149
+ content type: image/png
150
+ timestamp: 2023-01-31 19:34:47 UTC
151
+ genesis height: 774489
152
+ genesis fee: 5340
153
+ genesis transaction: acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37>acda637db995df796b35035fd978cc1a947f1e6fd5215968da88b7e38a7e4b37
154
+ location: 6630ff2153985504b180fc16721d559a4cecdc66f4be6acf33509ec2100c0aa5:0:0
155
+ output: 6630ff2153985504b180fc16721d559a4cecdc66f4be6acf33509ec2100c0aa5:0
156
+ offset: 0
157
+
158
+ =end
159
+
160
+ def inscription( id )
161
+ src = "#{@base}/#{@inscription}/#{id}"
162
+ res = get( src )
163
+
164
+ data = _parse_inscription( res.text )
165
+ data
166
+ end
167
+
168
+
169
+ def _parse_inscription( html )
170
+ doc = Nokogiri::HTML( html )
171
+
172
+ items = []
173
+
174
+ title = doc.css( 'head title' )
175
+ items << ['title', title.text]
176
+
177
+
178
+ dls = doc.css( 'body dl' )
179
+ dls[0].css( 'dt,dd' ).each do |el|
180
+ if el.name == 'dt'
181
+ items << [el.text]
182
+ elsif el.name == 'dd'
183
+ items[-1] << el.text
184
+ else
185
+ puts "!! ERROR - unexpected tag; expected dd|dl; got: #{el.name}"
186
+ exit 1
187
+ end
188
+ end
189
+ items
190
+
191
+ ## convert to hash
192
+ ## and check for duplicate
193
+ data = {}
194
+ items.each do |k,v|
195
+ k = k.strip
196
+ v = v.strip
197
+ if data.has_key?( k )
198
+ puts "!! ERROR - duplicate key >#{k}< in:"
199
+ pp items
200
+ exit 1
201
+ end
202
+ data[ k ] = v
203
+ end
204
+ data
205
+ end
206
+
207
+
208
+ def get( src )
209
+ res = Webclient.get( src )
210
+ if res.status.ok?
211
+ res
212
+ else
213
+ puts "!! ERROR - HTTP #{res.status.code} #{res.status.message} - failed web request >#{src}<; sorry"
214
+ exit 1
215
+ end
216
+ end
217
+ end # class Api
218
+
219
+ ##
220
+ ## add convenience alias
221
+ API = Api
222
+
223
+ end ## module Ordinals
224
+
@@ -0,0 +1,256 @@
1
+
2
+ module Ordinals
3
+
4
+
5
+ class Collection
6
+ attr_reader :slug,
7
+ :width, :height,
8
+ :sources
9
+
10
+
11
+ def initialize( slug )
12
+ @slug = slug
13
+
14
+ ## read config if present
15
+ config_path = "./#{@slug}/collection.yml"
16
+ if File.exist?( config_path )
17
+ config = read_yaml( config_path )
18
+ pp config
19
+
20
+ @width, @height = _parse_dimension( config['format'] )
21
+
22
+ ## note: allow multiple source formats / dimensions
23
+ ### e.g. convert 512x512 into [ [512,512] ]
24
+ ##
25
+ source = config['source']
26
+ source = [source] unless source.is_a?( Array )
27
+ @sources = source.map { |dimension| _parse_dimension( dimension ) }
28
+ end
29
+ end
30
+
31
+
32
+ def ordinals
33
+ @ordinals ||= begin
34
+ recs = read_csv( "./#{@slug}/ordinals.csv" )
35
+ puts " #{recs.size} record(s)"
36
+ recs
37
+ end
38
+ @ordinals
39
+ end
40
+
41
+ def count() ordinals.size; end
42
+ alias_method :size, :count
43
+
44
+
45
+
46
+ def each_ordinal( &block )
47
+ ordinals.each_with_index do |rec, i| ## pass along hash rec for now - why? why not?
48
+ block.call( rec, i )
49
+ end
50
+ end
51
+
52
+ ## add each_source_image or each_token_image or each_original_image or such - why? why not??
53
+
54
+
55
+
56
+ def each_image( &block )
57
+ each_ordinal do |rec, i|
58
+ id = rec['id']
59
+ num = rec.has_key?('num') ? rec['num'].to_i(10) : i+1
60
+
61
+ path = "./#{@slug}/#{@width}x#{@height}/#{num}.png"
62
+ img = Image.read( path )
63
+
64
+ block.call( img, num )
65
+ end
66
+ end
67
+
68
+
69
+
70
+ def image_dir() "./#{@slug}/token-i"; end
71
+
72
+
73
+
74
+ ## e.g. convert dimension (width x height) "24x24" or "24 x 24" to [24,24]
75
+ def _parse_dimension( str )
76
+ str.split( /x/i ).map { |str| str.strip.to_i }
77
+ end
78
+
79
+
80
+ def pixelate
81
+ each_ordinal do |rec, i|
82
+ id = rec['id']
83
+ num = rec.has_key?('num') ? rec['num'].to_i(10) : i+1
84
+
85
+ outpath = "./#{@slug}/#{@width}x#{@height}/#{num}.png"
86
+ next if File.exist?( outpath )
87
+
88
+ path = "#{image_dir}/#{num}.png"
89
+ puts "==> reading #{path}..."
90
+
91
+ img = Image.read( path )
92
+ puts " #{img.width}x#{img.height}"
93
+
94
+ ## check for source images
95
+ if !@sources.include?( [img.width, img.height] )
96
+ puts " !! ERROR - unexpected image size; sorry - expected:"
97
+ pp @sources
98
+ exit 1
99
+ end
100
+
101
+ ## check for special case source == format!!
102
+ if [img.width,img.height] == [@width,@height]
103
+ puts " note: saving image as is - no downsampling"
104
+ img.save( outpath )
105
+ else
106
+ steps_x = Image.calc_sample_steps( img.width, @width )
107
+ steps_y = Image.calc_sample_steps( img.height, @height )
108
+
109
+ img = img.sample( steps_x, steps_y )
110
+ img.save( outpath )
111
+ end
112
+ end
113
+ end
114
+
115
+
116
+
117
+
118
+ def make_composite
119
+ cols, rows = case count
120
+ when 10 then [5, 2]
121
+ when 12 then [4, 3]
122
+ when 15 then [5, 3]
123
+ when 20 then [5, 4]
124
+ when 50 then [10, 5]
125
+ when 69 then [10, 7]
126
+ when 80 then [10, 8]
127
+ when 88 then [10, 9]
128
+ when 98,99 then [10, 10]
129
+ when 100 then [10, 10]
130
+ when 101 then [11, 10]
131
+ when 111 then [11, 11]
132
+ when 130 then [10, 13]
133
+ when 512 then [20, 26]
134
+ else
135
+ raise ArgumentError, "sorry - unknown composite count #{count} for now"
136
+ end
137
+
138
+ composite = ImageComposite.new( cols, rows,
139
+ width: @width,
140
+ height: @height )
141
+
142
+
143
+ each_image do |img, num|
144
+ puts "==> #{num}"
145
+ composite << img
146
+ end
147
+
148
+ composite.save( "./#{@slug}/tmp/#{@slug}.png" )
149
+ end
150
+
151
+
152
+
153
+ def convert_images
154
+ Image.convert( image_dir, from: 'jpg',
155
+ to: 'png' )
156
+
157
+ Image.convert( image_dir, from: 'gif',
158
+ to: 'png' )
159
+
160
+ Image.convert( image_dir, from: 'webp',
161
+ to: 'png' )
162
+ end
163
+
164
+
165
+
166
+ def fix_images ## todo - find a different names for resaving png images?
167
+ ## "repair" png images
168
+ ## starting w/
169
+ ## - why?
170
+ ##
171
+ ## verify_signature! - ChunkyPNG::SignatureMismatch:
172
+ ## PNG signature not found,
173
+ ## found "RIFF\\xFE\\b\\x00\\x00"
174
+ ## instead of "\\x89PNG\\r\\n\\x1A\\n"
175
+
176
+ Image.convert( image_dir, from: 'png',
177
+ to: 'png' )
178
+ end
179
+
180
+
181
+ def download_meta ## inscription metadata
182
+ each_ordinal do |rec, i|
183
+ id = rec['id']
184
+ num = rec.has_key?('num') ? rec['num'].to_i(10) : i+1
185
+
186
+ chain = Ordinals.config.chain
187
+ path = "../ordinals.cache/#{chain}/#{id}.json"
188
+ next if File.exist?( path )
189
+
190
+ puts "==> downloading #{chain} inscription meta #{num} w/ id #{id}..."
191
+
192
+ data = Ordinals.client.inscription( id )
193
+
194
+ write_json( path, data )
195
+
196
+ sleep( 1.0 ) ## sleep (delay_in_s)
197
+ end
198
+ end
199
+
200
+ def dump_stats
201
+ stats = InscriptionStats.new
202
+
203
+ each_ordinal do |rec, i|
204
+ id = rec['id']
205
+
206
+ chain = Ordinals.config.chain
207
+ path = "../ordinals.cache/#{chain}/#{id}.json"
208
+
209
+ data = read_json( path )
210
+ stats.update( data )
211
+ end
212
+
213
+ pp stats.data
214
+ puts
215
+ puts stats.format
216
+ end
217
+
218
+
219
+ def download_images
220
+ each_ordinal do |rec, i|
221
+ id = rec['id']
222
+ num = rec.has_key?('num') ? rec['num'].to_i(10) : i+1
223
+
224
+ next if File.exist?( "#{image_dir}/#{num}.png" )
225
+
226
+ puts "==> downloading image ##{num}..."
227
+
228
+
229
+ content = Ordinals.client.content( id )
230
+
231
+ puts " content_type: #{content.type}, content_length: #{content.length}"
232
+
233
+ format = if content.type =~ %r{image/jpeg}i
234
+ 'jpg'
235
+ elsif content.type =~ %r{image/png}i
236
+ 'png'
237
+ elsif content.type =~ %r{image/gif}i
238
+ 'gif'
239
+ elsif content.type =~ %r{image/webp}i
240
+ 'webp'
241
+ else
242
+ puts "!! ERROR:"
243
+ puts " unknown image format content type: >#{content.type}<"
244
+ exit 1
245
+ end
246
+
247
+ ## save image - using b(inary) mode
248
+ write_blob( "#{image_dir}/#{num}.#{format}", content.blob )
249
+
250
+ sleep( 1.0 ) ## sleep (delay_in_s)
251
+ end
252
+ end
253
+
254
+ end # class Collection
255
+ end # module Ordinals
256
+
@@ -0,0 +1,142 @@
1
+
2
+
3
+ class InscriptionStats
4
+
5
+ TITLE_RX = /^Inscription[ ]+(?<ord>[0-9]+)$/
6
+ BYTES_RX = /^(?<bytes>[0-9]+)[ ]+bytes$/
7
+
8
+ def initialize
9
+ @stats = {
10
+ count: 0,
11
+ ord: { '<1000' => 0,
12
+ '<10000' => 0,
13
+ '<100000' => 0,
14
+ '<1000000' => 0,
15
+ 'min' => nil,
16
+ 'max' => nil, },
17
+ type: Hash.new(0),
18
+ bytes: { '<100' => 0,
19
+ '<1000' => 0,
20
+ '<10000' => 0,
21
+ '<100000' => 0,
22
+ '<1000000' => 0,
23
+ 'min' => nil,
24
+ 'max' => nil, },
25
+ }
26
+
27
+ @days = Hash.new(0)
28
+ end
29
+
30
+ def size() @stats[:count]; end
31
+
32
+
33
+
34
+ def update( data )
35
+ data = JSON.parse( data) if data.is_a?( String )
36
+
37
+ @stats[ :count ] += 1
38
+
39
+
40
+ ord = nil
41
+ if m=TITLE_RX.match( data['title'] )
42
+ ord = m[:ord].to_i(10)
43
+ else
44
+ puts "!! ERROR - cannot find ord in inscription title; sorry"
45
+ pp data
46
+ exit 1
47
+ end
48
+
49
+ if ord < 1000 then @stats[:ord]['<1000'] += 1
50
+ elsif ord < 10000 then @stats[:ord]['<10000'] += 1
51
+ elsif ord < 100000 then @stats[:ord]['<100000'] += 1
52
+ elsif ord < 1000000 then @stats[:ord]['<1000000'] += 1
53
+ else
54
+ puts "!! ERROR ord out-of-bounds"
55
+ exit 1
56
+ end
57
+
58
+ ## update min/max
59
+ @stats[:ord]['min'] = ord if ord < (@stats[:ord]['min'] || 9999999)
60
+ @stats[:ord]['max'] = ord if ord > (@stats[:ord]['max'] || -1)
61
+
62
+
63
+ bytes = nil
64
+ if m=BYTES_RX.match( data['content length'] )
65
+ bytes = m[:bytes].to_i(10)
66
+ else
67
+ puts "!! ERROR - cannot find bytes in inscription content length; sorry"
68
+ pp data
69
+ exit 1
70
+ end
71
+
72
+ if bytes < 100 then @stats[:bytes]['<100'] += 1
73
+ elsif bytes < 1000 then @stats[:bytes]['<1000'] += 1
74
+ elsif bytes < 10000 then @stats[:bytes]['<10000'] += 1
75
+ elsif bytes < 100000 then @stats[:bytes]['<100000'] += 1
76
+ elsif bytes < 1000000 then @stats[:bytes]['<1000000'] += 1
77
+ else
78
+ puts "!! ERROR bytes (content-length) out-of-bounds"
79
+ exit 1
80
+ end
81
+
82
+ ## update min/max
83
+ @stats[:bytes]['min'] = bytes if bytes < (@stats[:bytes]['min'] || 9999999)
84
+ @stats[:bytes]['max'] = bytes if bytes > (@stats[:bytes]['max'] || -1)
85
+
86
+
87
+ type = data['content type']
88
+
89
+ @stats[:type][ type ] += 1
90
+
91
+ ## "timestamp"=>"2023-02-05 01:45:22 UTC",
92
+ ts = Time.strptime( data['timestamp'], '%Y-%m-%d %H:%M:%S' )
93
+
94
+ @days[ ts.strftime('%Y-%m-%d') ] +=1
95
+ end # method update
96
+
97
+
98
+ def data ## rename to model or such - why? why not?
99
+ ## sort days
100
+ days = @days.sort {|l,r| l[0] <=> r[0] }
101
+ ## add to stats
102
+ @stats[:days] = {}
103
+ days.each {|k,v| @stats[:days][k] = v }
104
+ @stats
105
+ end
106
+
107
+ =begin
108
+ {:count=>101,
109
+ :ord=>{"<1000"=>0, "<10000"=>0, "<100000"=>101, "<1000000"=>0, "min"=>14343, "max"=>42901},
110
+ :type=>{"image/png"=>101},
111
+ :bytes=>{"<100"=>0, "<1000"=>101, "<10000"=>0, "<100000"=>0, "<1000000"=>0, "min"=>274, "max"=>512},
112
+ :days=>{"2023-02-22"=>10, "2023-02-25"=>17, "2023-02-26"=>74}}
113
+ =end
114
+
115
+ def format ## rename to pretty_print or such - why? why not?
116
+ stat = data
117
+
118
+ buf = String.new('')
119
+ buf << "#{stat[:count]} inscription(s)\n"
120
+
121
+ buf << "- from ##{stat[:ord]['min']} to ##{stat[:ord]['max']} (min. to max.)\n"
122
+ buf << " - <1000 => #{stat[:ord]['<1000']}\n" if stat[:ord]["<1000"] > 0
123
+ buf << " - <10000 => #{stat[:ord]['<10000']}\n" if stat[:ord]["<10000"] > 0
124
+ buf << " - <100000 => #{stat[:ord]['<100000']}\n" if stat[:ord]["<100000"] > 0
125
+ buf << " - <1000000 => #{stat[:ord]['<1000000']}\n" if stat[:ord]["<1000000"] > 0
126
+
127
+ buf << "- format(s)\n"
128
+ stat[:type].each do |k,v|
129
+ buf << " - #{k} => #{v}\n"
130
+ end
131
+
132
+ buf << "- day(s)\n"
133
+ stat[:days].each do |k,v|
134
+ buf << " - #{k} => #{v}\n"
135
+ end
136
+
137
+ buf
138
+ end
139
+
140
+ end ## class InscriptionStats
141
+
142
+
@@ -0,0 +1,121 @@
1
+ module Ordinals
2
+ class Tool
3
+
4
+ def self.main( args=ARGV )
5
+ puts "==> welcome to ordinals/ordbase tool with args:"
6
+ pp args
7
+
8
+ options = {
9
+ }
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.on( "--doge", "--dogecoin", "Use Dogecoin / DOGE") do
13
+ ## switch to doge
14
+ Ordinals.config.chain = :doge
15
+ end
16
+ opts.on( "--ltc", "--litecoin", "Use Litecoin / LTC") do
17
+ ## switch to ltc
18
+ Ordinals.config.chain = :ltc
19
+ end
20
+ opts.on( "--btc", "--bitcoin", "Use Bitcoin / BTC") do
21
+ ## switch to btc (default)
22
+ Ordinals.config.chain = :btc
23
+ end
24
+
25
+ opts.on("-h", "--help", "Prints this help") do
26
+ puts opts
27
+ exit
28
+ end
29
+ end
30
+
31
+ parser.parse!( args )
32
+ puts "options:"
33
+ pp options
34
+
35
+ puts "args:"
36
+ pp args
37
+
38
+ if args.size < 1
39
+ puts "!! ERROR - no collection found - use <collection> <command>..."
40
+ puts ""
41
+ exit
42
+ end
43
+
44
+ slug = args[0]
45
+ command = args[1] || 'image'
46
+
47
+ if ['m', 'meta'].include?( command )
48
+ do_download_meta( slug )
49
+ elsif ['stat', 'stats'].include?( command )
50
+ do_dump_stats( slug )
51
+ elsif ['i','img', 'image'].include?( command )
52
+ do_download_images( slug )
53
+ elsif ['conv','convert'].include?( command )
54
+ do_convert_images( slug )
55
+ elsif ['fix'].include?( command )
56
+ do_fix_images( slug )
57
+ elsif ['px','pix', 'pixelate' ].include?( command )
58
+ do_pixelate( slug )
59
+ elsif ['comp','composite' ].include?( command )
60
+ do_make_composite( slug )
61
+ else
62
+ puts "!! ERROR - unknown command >#{command}<, sorry"
63
+ end
64
+
65
+ puts "bye"
66
+ end
67
+
68
+
69
+ def self.do_dump_stats( slug )
70
+ puts "==> dump inscription stats for collection >#{slug}<..."
71
+
72
+ col = Collection.new( slug )
73
+ col.dump_stats
74
+ end
75
+
76
+ def self.do_download_meta( slug )
77
+ puts "==> download meta for collection >#{slug}<..."
78
+
79
+ col = Collection.new( slug )
80
+ col.download_meta
81
+ end
82
+
83
+ def self.do_download_images( slug )
84
+ puts "==> download images for collection >#{slug}<..."
85
+
86
+ col = Collection.new( slug )
87
+ col.download_images
88
+ end
89
+
90
+
91
+ def self.do_convert_images( slug )
92
+ puts "==> convert images for collection >#{slug}<..."
93
+
94
+ col = Collection.new( slug )
95
+ col.convert_images
96
+ end
97
+
98
+ def self.do_fix_images( slug )
99
+ puts "==> fix images for collection >#{slug}<..."
100
+
101
+ col = Collection.new( slug )
102
+ col.fix_images
103
+ end
104
+
105
+
106
+ def self.do_pixelate( slug )
107
+ puts "==> downsample / pixelate images for collection >#{slug}<..."
108
+
109
+ col = Collection.new( slug )
110
+ col.pixelate
111
+ end
112
+
113
+ def self.do_make_composite( slug )
114
+ puts "==> make composite for collection >#{slug}<..."
115
+
116
+ col = Collection.new( slug )
117
+ col.make_composite
118
+ end
119
+
120
+ end # class Tool
121
+ end # module Ordinals
data/lib/ordinals.rb CHANGED
@@ -1,3 +1,92 @@
1
1
 
2
- puts "hello ordinals"
2
+ require 'cocos'
3
+ require 'pixelart'
4
+
5
+ require 'optparse'
6
+
7
+
8
+ ## add nokogiri for api (html parsing)
9
+ require 'nokogiri'
10
+
11
+
12
+
13
+
14
+
15
+ module Ordinals
16
+ class Configuration
17
+
18
+ #######################
19
+ ## accessors
20
+ def chain=(value)
21
+ if value.is_a?( String ) || value.is_a?( Symbol )
22
+ case value.downcase.to_s
23
+ when 'btc', 'bitcoin', 'bitcon'
24
+ @chain = 'btc'
25
+ @client = Ordinals::Api.bitcoin
26
+ when 'ltc', 'litecoin', 'litecon'
27
+ @chain = 'ltc'
28
+ @client = Ordinals::Api.litecoin
29
+ when 'doge', 'dogecoin', 'dogecon'
30
+ @chain = 'doge'
31
+ @client = Ordinals::Api.dogecoin
32
+ else
33
+ raise ArgumentError, "unknown chain - expected btc | ltc | doge; got #{value}"
34
+ end
35
+ else
36
+ raise ArgumentError, "only string or symbol supported for now; sorry - got: #{value.inspect} : #{value.class.name}"
37
+ end
38
+ end
39
+
40
+ def chain
41
+ ## note - default to btc/bitcon if not set
42
+ self.chain = 'btc' unless defined?( @chain )
43
+ @chain
44
+ end
45
+
46
+ ## note: read-only for now - why? why not?
47
+ def client
48
+ ## note - default to btc/bitcon if not set
49
+ self.chain = 'btc' unless defined?( @client )
50
+ @client
51
+ end
52
+ end # class Configuration
53
+
54
+
55
+ ## lets you use
56
+ ## Ordinals.configure do |config|
57
+ ## config.chain = :btc
58
+ ## end
59
+ def self.configure() yield( config ); end
60
+ def self.config() @config ||= Configuration.new; end
61
+
62
+ ## add some convenience shortcut helpers (no config. required) - why? why not?
63
+ def self.client() config.client; end
64
+ def self.chain() config.chain; end
65
+
66
+ def self.btc?() config.chain == 'btc'; end
67
+ def self.ltc?() config.chain == 'ltc'; end
68
+ def self.doge?() config.chain == 'doge'; end
69
+ class << self
70
+ alias_method :bitcoin?, :btc?
71
+ alias_method :bitcon?, :btc?
72
+
73
+ alias_method :litecoin?, :ltc?
74
+ alias_method :litecon?, :ltc?
75
+
76
+ alias_method :dogecoin?, :doge?
77
+ alias_method :dogecon?, :doge?
78
+ end
79
+ end # module Ordinals
80
+
81
+
82
+
83
+
84
+ ## our own code
85
+ require_relative 'ordinals/api'
86
+ require_relative 'ordinals/collection'
87
+ require_relative 'ordinals/stats'
88
+
89
+ require_relative 'ordinals/tool'
90
+
91
+
3
92
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ordinals
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-11 00:00:00.000000000 Z
11
+ date: 2023-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cocos
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rdoc
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,10 +86,11 @@ dependencies:
72
86
  - - "~>"
73
87
  - !ruby/object:Gem::Version
74
88
  version: '3.23'
75
- description: ordinals gem - (off-chain) ordinals (pixel art) machinery & helpers for
76
- Bitcoin & co.
89
+ description: ordinals gem - "right-clicker" (off-chain) ordinals (pixel art) machinery
90
+ & helpers for Bitcoin, Litecoin, Dogecoin & co.
77
91
  email: wwwmake@googlegroups.com
78
- executables: []
92
+ executables:
93
+ - ordbase
79
94
  extensions: []
80
95
  extra_rdoc_files:
81
96
  - CHANGELOG.md
@@ -86,7 +101,12 @@ files:
86
101
  - Manifest.txt
87
102
  - README.md
88
103
  - Rakefile
104
+ - bin/ordbase
89
105
  - lib/ordinals.rb
106
+ - lib/ordinals/api.rb
107
+ - lib/ordinals/collection.rb
108
+ - lib/ordinals/stats.rb
109
+ - lib/ordinals/tool.rb
90
110
  homepage: https://github.com/pixelartexchange/ordinals.sandbox
91
111
  licenses:
92
112
  - Public Domain
@@ -111,6 +131,6 @@ requirements: []
111
131
  rubygems_version: 3.3.7
112
132
  signing_key:
113
133
  specification_version: 4
114
- summary: ordinals gem - (off-chain) ordinals (pixel art) machinery & helpers for Bitcoin
115
- & co.
134
+ summary: ordinals gem - "right-clicker" (off-chain) ordinals (pixel art) machinery
135
+ & helpers for Bitcoin, Litecoin, Dogecoin & co.
116
136
  test_files: []