ordinals 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []