trident_assistant 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +132 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +106 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/ta +6 -0
- data/lib/trident_assistant/api/collectible.rb +81 -0
- data/lib/trident_assistant/api/collection.rb +61 -0
- data/lib/trident_assistant/api/metadata.rb +26 -0
- data/lib/trident_assistant/api/order.rb +77 -0
- data/lib/trident_assistant/api.rb +27 -0
- data/lib/trident_assistant/cli/base.rb +55 -0
- data/lib/trident_assistant/cli/collectible.rb +100 -0
- data/lib/trident_assistant/cli/collection.rb +81 -0
- data/lib/trident_assistant/cli/metadata.rb +119 -0
- data/lib/trident_assistant/cli/nfo.rb +101 -0
- data/lib/trident_assistant/cli/order.rb +77 -0
- data/lib/trident_assistant/cli/utils.rb +23 -0
- data/lib/trident_assistant/cli.rb +45 -0
- data/lib/trident_assistant/client.rb +116 -0
- data/lib/trident_assistant/utils/memo.rb +62 -0
- data/lib/trident_assistant/utils/metadata.rb +117 -0
- data/lib/trident_assistant/utils.rb +88 -0
- data/lib/trident_assistant/version.rb +5 -0
- data/lib/trident_assistant.rb +18 -0
- metadata +92 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TridentAssistant
|
4
|
+
class API
|
5
|
+
# api for order
|
6
|
+
module Order
|
7
|
+
EXCHANGE_ASSET_ID = "965e5c6e-434c-3fa9-b780-c50f43cd955c"
|
8
|
+
MINIMUM_AMOUNT = 0.000_000_01
|
9
|
+
|
10
|
+
def orders(**kwargs)
|
11
|
+
client.get(
|
12
|
+
"api/orders",
|
13
|
+
headers: {
|
14
|
+
Authorization: "Bearer #{mixin_bot.access_token("GET", "/me")}"
|
15
|
+
},
|
16
|
+
params: {
|
17
|
+
collection_id: kwargs[:collection_id],
|
18
|
+
metahash: kwargs[:metahash],
|
19
|
+
state: kwargs[:state],
|
20
|
+
type: kwargs[:type]
|
21
|
+
}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def order(id)
|
26
|
+
client
|
27
|
+
.get(
|
28
|
+
"api/orders/#{id}",
|
29
|
+
headers: {
|
30
|
+
Authorization: "Bearer #{mixin_bot.access_token("GET", "/me")}"
|
31
|
+
}
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def fill_order(order_id)
|
36
|
+
info = order order_id
|
37
|
+
raise "Order state: #{info["state"]}" if info["state"] != "open"
|
38
|
+
|
39
|
+
memo = TridentAssistant::Utils::Memo.new(type: "F", order_id: order_id, token_id: info["token_id"])
|
40
|
+
|
41
|
+
trace_id = SecureRandom.uuid
|
42
|
+
mixin_bot.create_multisig_transaction(
|
43
|
+
keystore[:pin],
|
44
|
+
{
|
45
|
+
asset_id: info["asset_id"],
|
46
|
+
trace_id: trace_id,
|
47
|
+
amount: info["price"],
|
48
|
+
memo: memo.encode,
|
49
|
+
receivers: TridentAssistant::Utils::TRIDENT_MTG[:members],
|
50
|
+
threshold: TridentAssistant::Utils::TRIDENT_MTG[:threshold]
|
51
|
+
}
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def cancel_order(order_id)
|
56
|
+
info = order order_id
|
57
|
+
raise "Order maker: #{info["maker"]["id"]}" if info.dig("maker", "id") != mixin_bot.client_id
|
58
|
+
raise "Order state: #{info["state"]}" if info["state"] != "open"
|
59
|
+
|
60
|
+
memo = TridentAssistant::Utils::Memo.new(type: "C", order_id: order_id, token_id: info["token_id"])
|
61
|
+
|
62
|
+
trace_id = SecureRandom.uuid
|
63
|
+
mixin_bot.create_multisig_transaction(
|
64
|
+
keystore[:pin],
|
65
|
+
{
|
66
|
+
asset_id: info["asset_id"],
|
67
|
+
trace_id: trace_id,
|
68
|
+
amount: info["price"],
|
69
|
+
memo: memo.encode,
|
70
|
+
receivers: TridentAssistant::Utils::TRIDENT_MTG[:members],
|
71
|
+
threshold: TridentAssistant::Utils::TRIDENT_MTG[:threshold]
|
72
|
+
}
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./client"
|
4
|
+
require_relative "./api/collection"
|
5
|
+
require_relative "./api/collectible"
|
6
|
+
require_relative "./api/metadata"
|
7
|
+
require_relative "./api/order"
|
8
|
+
|
9
|
+
module TridentAssistant
|
10
|
+
# APIs of Trident server
|
11
|
+
class API
|
12
|
+
attr_reader :mixin_bot, :client, :keystore
|
13
|
+
|
14
|
+
def initialize(**args)
|
15
|
+
@client = Client.new endpoint: args[:endpoint]
|
16
|
+
return if args[:keystore].blank?
|
17
|
+
|
18
|
+
@keystore = TridentAssistant::Utils.parse_json args[:keystore]
|
19
|
+
@mixin_bot = TridentAssistant::Utils.mixin_bot_from_keystore args[:keystore]
|
20
|
+
end
|
21
|
+
|
22
|
+
include TridentAssistant::API::Collectible
|
23
|
+
include TridentAssistant::API::Collection
|
24
|
+
include TridentAssistant::API::Metadata
|
25
|
+
include TridentAssistant::API::Order
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../api"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# Base class of CLI
|
8
|
+
class Base < Thor
|
9
|
+
# https://github.com/Shopify/cli-ui
|
10
|
+
UI = ::CLI::UI
|
11
|
+
|
12
|
+
attr_reader :keystore, :bot, :client, :api
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
super
|
16
|
+
|
17
|
+
endpoint =
|
18
|
+
if options[:endpoint].present?
|
19
|
+
options[:endpoint]
|
20
|
+
else
|
21
|
+
{
|
22
|
+
prod: "https://thetrident.one",
|
23
|
+
test: "https://trident-test.onrender.com",
|
24
|
+
dev: "http://localhost:3000"
|
25
|
+
}[options[:environment].to_sym]
|
26
|
+
end
|
27
|
+
@api =
|
28
|
+
begin
|
29
|
+
TridentAssistant::API.new(
|
30
|
+
endpoint: endpoint,
|
31
|
+
keystore: options[:keystore]
|
32
|
+
)
|
33
|
+
rescue JSON::ParserError
|
34
|
+
log UI.fmt("{{x}} falied to parse keystore.json: #{options[:keystore]}")
|
35
|
+
rescue StandardError => e
|
36
|
+
log UI.fmt "{{x}} Failed to initialize Mixin bot, maybe your keystore is incorrect. #{e.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def log(obj)
|
43
|
+
if options[:pretty]
|
44
|
+
if obj.is_a? String
|
45
|
+
puts obj
|
46
|
+
else
|
47
|
+
ap obj
|
48
|
+
end
|
49
|
+
else
|
50
|
+
puts obj.inspect
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# CLI to query Trident and Mixin APIs
|
8
|
+
class Collectible < Base
|
9
|
+
desc "index", "query collectibles in wallet"
|
10
|
+
option :state, type: :string, aliases: "s", required: false, default: :unspent,
|
11
|
+
desc: "keystore or keystore.json file of Mixin bot"
|
12
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
13
|
+
def index
|
14
|
+
r = api.mixin_bot.collectibles state: options[:state]
|
15
|
+
|
16
|
+
log r["data"]
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "show COLLECTION TOKEN", "query collectible"
|
20
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
21
|
+
def show(collection, token)
|
22
|
+
token_id = MixinBot::Utils::Nfo.new(collection: collection, token: token).unique_token_id
|
23
|
+
r = api.mixin_bot.collectible token_id
|
24
|
+
|
25
|
+
log r["data"]
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "deposit COLLECTION TOKEN", "deposit NFT"
|
29
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
30
|
+
def deposit(collection, token)
|
31
|
+
log api.deposit collection, token
|
32
|
+
log UI.fmt("{{v}} successfully transfer NFT")
|
33
|
+
rescue StandardError => e
|
34
|
+
log UI.fmt("{{x}} failed: #{e.inspect} #{e.backtrace.join("\n")}")
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "airdrop COLLECTION, TOKEN", "airdrop NFT"
|
38
|
+
option :receiver, type: :string, aliases: "r", required: false, desc: "receiver ID of airdrop"
|
39
|
+
option :start, type: :string, aliases: "s", required: false, desc: "start time of airdrop"
|
40
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
41
|
+
def airdrop(collection, token)
|
42
|
+
log api.airdrop collection, token, receiver_id: options[:receiver], start_at: options[:start]
|
43
|
+
log UI.fmt("{{v}} successfully transfer NFT")
|
44
|
+
rescue StandardError => e
|
45
|
+
log UI.fmt("{{x}} failed: #{e.inspect} #{e.backtrace.join("\n")}")
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "withdraw COLLECTION TOKEN", "withdraw NFT"
|
49
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
50
|
+
def withdraw(collection, token)
|
51
|
+
log api.withdraw collection, token
|
52
|
+
log UI.fmt("{{v}} successfully invoked withdraw")
|
53
|
+
rescue StandardError => e
|
54
|
+
log UI.fmt("{{x}} failed: #{e.inspect} #{e.backtrace.join("\n")}")
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "bulkairdrop DIR", "Airdrop NFT in bulk"
|
58
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
59
|
+
def bulkairdrop(dir)
|
60
|
+
raise "#{dir} is not a directory" unless Dir.exist?(dir)
|
61
|
+
|
62
|
+
Dir.glob("#{dir}/*.json").each do |file|
|
63
|
+
log UI.fmt("{{v}} found #{file}")
|
64
|
+
data = TridentAssistant::Utils.parse_json file
|
65
|
+
metadata = TridentAssistant::Utils.parse_metadata data
|
66
|
+
log UI.fmt("{{v}} metadata parsed")
|
67
|
+
metadata.validate!
|
68
|
+
log UI.fmt("{{v}} metadata validated")
|
69
|
+
|
70
|
+
if data.dig("_airdrop", "hash").present?
|
71
|
+
log UI.fmt("{{v}} NFT already transferred")
|
72
|
+
next
|
73
|
+
end
|
74
|
+
|
75
|
+
receiver_id = data.dig("_airdrop", "receiver_id")
|
76
|
+
start_at = data.dig("_airdrop", "start_at")
|
77
|
+
log UI.fmt("{{v}} airdrop receiver_id: #{receiver_id}")
|
78
|
+
log UI.fmt("{{v}} airdrop start_at: #{start_at}")
|
79
|
+
|
80
|
+
r = api.airdrop metadata.collection["id"], metadata.token["id"], receiver_id: receiver_id, start_at: start_at
|
81
|
+
log r["data"]
|
82
|
+
data["_airdrop"] ||= {}
|
83
|
+
data["_airdrop"]["hash"] = r["data"]["hash"]
|
84
|
+
log UI.fmt("{{v}} successfully transfer NFT ##{metadata.token["id"]} #{metadata.collection["id"]}")
|
85
|
+
rescue TridentAssistant::Utils::Metadata::InvalidFormatError, JSON::ParserError, Client::RequestError,
|
86
|
+
MixinBot::Error, RuntimeError => e
|
87
|
+
log UI.fmt("{{x}} #{file} failed to airdrop: #{e.inspect}")
|
88
|
+
next
|
89
|
+
ensure
|
90
|
+
File.write file, data.to_json
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def _airdrop
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# CLI tool of collection
|
8
|
+
class Collection < Base
|
9
|
+
class InvalidError < TridentAssistant::Error; end
|
10
|
+
|
11
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
12
|
+
desc "index", "query all collections"
|
13
|
+
def index
|
14
|
+
log api.collections
|
15
|
+
end
|
16
|
+
|
17
|
+
option :name, type: :string, aliases: "n", desc: "collection Name"
|
18
|
+
option :description, type: :string, aliases: "d", desc: "collection Description"
|
19
|
+
option :icon, type: :string, aliases: "i", desc: "collection Icon"
|
20
|
+
option :url, type: :string, aliases: "u", desc: "collection External URL"
|
21
|
+
option :split, type: :string, aliases: "s", desc: "collection Split"
|
22
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
23
|
+
desc "create", "create a new collection"
|
24
|
+
def create
|
25
|
+
name = options[:name] || UI.ask("Please input collection name")
|
26
|
+
raise InvalidError, "Name cannot be blank" if name.blank?
|
27
|
+
|
28
|
+
description = options[:description] || UI.ask("Please input collection description")
|
29
|
+
raise InvalidError, "Description cannot be blank" if description.blank?
|
30
|
+
|
31
|
+
icon = options[:icon] || UI.ask("Please input icon file or icon url")
|
32
|
+
icon_file = File.open(icon) if File.exist? icon
|
33
|
+
|
34
|
+
external_url = options[:url] || UI.ask("Please input collection external url, start with https:// or http://")
|
35
|
+
split = options[:split] || UI.ask("Please input collection split", default: "0.0")
|
36
|
+
payload = {
|
37
|
+
name: name,
|
38
|
+
description: description,
|
39
|
+
external_url: external_url,
|
40
|
+
split: split
|
41
|
+
}
|
42
|
+
|
43
|
+
if icon_file.present?
|
44
|
+
payload[:icon_base64] = Base64.strict_encode64(icon_file.read)
|
45
|
+
else
|
46
|
+
payload[:icon_url] = icon
|
47
|
+
end
|
48
|
+
|
49
|
+
log api.create_collection(**payload)
|
50
|
+
rescue InvalidError => e
|
51
|
+
log UI.fmt("{{x}} failed: #{e.inspect}")
|
52
|
+
ensure
|
53
|
+
icon_file&.close
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "update ID", "update collection"
|
57
|
+
option :description, type: :string, aliases: "d", desc: "collection Description"
|
58
|
+
option :icon, type: :string, aliases: "i", desc: "collection Icon"
|
59
|
+
option :url, type: :string, aliases: "u", desc: "collection External URL"
|
60
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
61
|
+
def update(id)
|
62
|
+
payload = {}
|
63
|
+
payload[:description] = options[:description] if options[:description].present?
|
64
|
+
payload[:external_url] = options[:url] if options[:url].present?
|
65
|
+
if options[:icon].present? && File.exist?(options[:icon])
|
66
|
+
icon = File.open options[:icon]
|
67
|
+
payload[:icon_base64] = Base64.strict_encode64(icon.read)
|
68
|
+
end
|
69
|
+
log api.update_collection(id, **payload)
|
70
|
+
ensure
|
71
|
+
icon&.close
|
72
|
+
end
|
73
|
+
|
74
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
75
|
+
desc "show ID", "query a collection"
|
76
|
+
def show(id)
|
77
|
+
log api.collection id
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# CLI to generate metadata
|
8
|
+
class Metadata < Base
|
9
|
+
class InvalidError < TridentAssistant::Error; end
|
10
|
+
|
11
|
+
desc "new", "generate a new metadata"
|
12
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
13
|
+
def new
|
14
|
+
creator_id = api.mixin_bot.client_id
|
15
|
+
creator_name = api.mixin_bot.me["full_name"]
|
16
|
+
creator_royalty = UI.ask("Please input creator royalty, 0.0 ~ 0.1", default: "0.0")
|
17
|
+
raise InvalidError, "Royalty must in 0.0 ~ 0.1" unless (0..0.1).include?(creator_royalty.to_f)
|
18
|
+
|
19
|
+
collection_id = UI.ask("Please input collection ID")
|
20
|
+
collection = api.collection collection_id
|
21
|
+
raise InvalidError, "Cannot find collection #{collection_id}" if collection.blank?
|
22
|
+
|
23
|
+
if collection["creator"]&.[]("id") != api.mixin_bot.client_id
|
24
|
+
raise InvalidError,
|
25
|
+
"Unauthorized to mint in #{collection_id}"
|
26
|
+
end
|
27
|
+
|
28
|
+
token_id = UI.ask("Please input token ID", default: "1")
|
29
|
+
token_name = UI.ask("Please input token name")
|
30
|
+
token_description = UI.ask("Please input token description")
|
31
|
+
|
32
|
+
token_icon_url = UI.ask("Please input token icon url or local file dir")
|
33
|
+
if File.file? token_icon_url
|
34
|
+
begin
|
35
|
+
file = File.open token_icon_url
|
36
|
+
res = api.mixin_bot.upload_attachment file
|
37
|
+
token_icon_url = res["view_url"]
|
38
|
+
ensure
|
39
|
+
file&.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
token_media_url = UI.ask("Please input token media url or local file dir")
|
44
|
+
if File.file? token_media_url
|
45
|
+
begin
|
46
|
+
file = File.open token_media_url
|
47
|
+
res = api.mixin_bot.upload_attachment file
|
48
|
+
token_media_url = res["view_url"]
|
49
|
+
token_media_hash = SHA3::Digest::SHA256.hexdigest file.read
|
50
|
+
ensure
|
51
|
+
file&.close
|
52
|
+
end
|
53
|
+
elsif token_media_url =~ URI::DEFAULT_PARSER.make_regexp
|
54
|
+
token_media_hash = TridentAssistant::Utils.hash_from_url(token_media_url)
|
55
|
+
end
|
56
|
+
|
57
|
+
metadata = TridentAssistant::Utils::Metadata.new(
|
58
|
+
creator: {
|
59
|
+
id: creator_id,
|
60
|
+
name: creator_name,
|
61
|
+
royalty: creator_royalty
|
62
|
+
},
|
63
|
+
collection: {
|
64
|
+
id: collection_id,
|
65
|
+
name: collection["name"],
|
66
|
+
description: collection["description"],
|
67
|
+
icon: {
|
68
|
+
url: collection["icon"]&.[]("url")
|
69
|
+
},
|
70
|
+
split: collection["split"].to_s
|
71
|
+
},
|
72
|
+
token: {
|
73
|
+
id: token_id,
|
74
|
+
name: token_name,
|
75
|
+
description: token_description,
|
76
|
+
icon: {
|
77
|
+
url: token_icon_url
|
78
|
+
},
|
79
|
+
media: {
|
80
|
+
url: token_media_url,
|
81
|
+
hash: token_media_hash.to_s
|
82
|
+
}
|
83
|
+
},
|
84
|
+
checksum: {
|
85
|
+
fields: ["creator.id", "creator.royalty", "collection.id", "collection.name",
|
86
|
+
"collection.split", "token.id", "token.name", "token.media.hash"],
|
87
|
+
algorithm: "sha256"
|
88
|
+
}
|
89
|
+
)
|
90
|
+
|
91
|
+
File.write "#{token_id}_#{collection_id}.json", metadata.json.to_json
|
92
|
+
log metadata.json
|
93
|
+
end
|
94
|
+
|
95
|
+
desc "show METAHASH", "query metadata via metahash"
|
96
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
97
|
+
def show(metahash)
|
98
|
+
log api.metadata metahash
|
99
|
+
end
|
100
|
+
|
101
|
+
desc "upload", "upload metadata to Trident"
|
102
|
+
option :metadata, type: :string, aliases: "m", required: true, desc: "metadata or metadata.json file"
|
103
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
104
|
+
def upload
|
105
|
+
metadata = TridentAssistant::Utils.parse_metadata options[:metadata]
|
106
|
+
log UI.fmt("{{v}} metadata parsed")
|
107
|
+
|
108
|
+
metadata.validate!
|
109
|
+
log UI.fmt("{{v}} metadata validated")
|
110
|
+
|
111
|
+
# upload metadata
|
112
|
+
api.upload_metadata metadata: metadata.json, metahash: metadata.metahash
|
113
|
+
log UI.fmt("{{v}} metadata uploaded: #{options[:endpoint]}/api/collectibles/#{metadata.metahash}")
|
114
|
+
rescue JSON::ParserError, Client::RequestError, InvalidError => e
|
115
|
+
log UI.fmt("{{x}} #{e.inspect}")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# CLI to mint NFTs
|
8
|
+
class NFO < Base
|
9
|
+
desc "mint", "Mint NFT from NFO"
|
10
|
+
option :metadata, type: :string, aliases: "m", required: true, desc: "metadata or metadata.json file"
|
11
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
12
|
+
def mint
|
13
|
+
_mint options[:metadata]
|
14
|
+
rescue JSON::ParserError, Client::RequestError, TridentAssistant::Utils::Metadata::InvalidFormatError,
|
15
|
+
MixinBot::Error, RuntimeError => e
|
16
|
+
log UI.fmt("{{x}} #{e.inspect}")
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "bulkmint DIR", "Mint NFT in bulk"
|
20
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
21
|
+
def bulkmint(dir)
|
22
|
+
raise "#{dir} is not a directory" unless Dir.exist?(dir)
|
23
|
+
|
24
|
+
Dir.glob("#{dir}/*.json").each do |file|
|
25
|
+
log UI.fmt("{{v}} found #{file}")
|
26
|
+
_mint file
|
27
|
+
rescue TridentAssistant::Utils::Metadata::InvalidFormatError, JSON::ParserError, Client::RequestError,
|
28
|
+
MixinBot::Error, RuntimeError => e
|
29
|
+
log UI.fmt("{{x}} #{file} failed: #{e.inspect}")
|
30
|
+
next
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def _mint(raw)
|
37
|
+
# parse metadata
|
38
|
+
data = TridentAssistant::Utils.parse_json raw
|
39
|
+
metadata = TridentAssistant::Utils.parse_metadata data
|
40
|
+
log UI.fmt("{{v}} metadata parsed")
|
41
|
+
|
42
|
+
# validate metadata
|
43
|
+
metadata.validate!
|
44
|
+
log UI.fmt("{{v}} metadata validated")
|
45
|
+
|
46
|
+
raise "Creator ID incompatible with keystore" if metadata.creator[:id] != api.mixin_bot.client_id
|
47
|
+
|
48
|
+
token_id = MixinBot::Utils::Nfo.new(collection: metadata.collection[:id],
|
49
|
+
token: metadata.token[:id]).unique_token_id
|
50
|
+
collectible =
|
51
|
+
begin
|
52
|
+
api.mixin_bot.collectible token_id
|
53
|
+
rescue MixinBot::NotFoundError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
if collectible.present?
|
57
|
+
log UI.fmt("{{v}} already minted: #{token_id}")
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
# upload metadata
|
62
|
+
if data.dig("_mint", "metahash").blank?
|
63
|
+
api.upload_metadata metadata: metadata.json, metahash: metadata.metahash
|
64
|
+
data["_mint"] ||= {}
|
65
|
+
data["_mint"]["metahash"] = metadata.metahash
|
66
|
+
end
|
67
|
+
log UI.fmt("{{v}} metadata uploaded: #{options[:endpoint]}/api/collectibles/#{metadata.metahash}")
|
68
|
+
|
69
|
+
# pay to NFO
|
70
|
+
trace_id = data.dig("_mint", "trace_id") || SecureRandom.uuid
|
71
|
+
memo = api.mixin_bot.nft_memo metadata.collection[:id], metadata.token[:id].to_i, metadata.metahash
|
72
|
+
payment =
|
73
|
+
api.mixin_bot.create_multisig_transaction(
|
74
|
+
api.keystore[:pin],
|
75
|
+
{
|
76
|
+
asset_id: TridentAssistant::Utils::MINT_ASSET_ID,
|
77
|
+
trace_id: trace_id,
|
78
|
+
amount: TridentAssistant::Utils::MINT_AMOUNT,
|
79
|
+
memo: memo,
|
80
|
+
receivers: TridentAssistant::Utils::NFO_MTG[:members],
|
81
|
+
threshold: TridentAssistant::Utils::NFO_MTG[:threshold]
|
82
|
+
}
|
83
|
+
)
|
84
|
+
|
85
|
+
data["_mint"]["trace_id"] = trace_id
|
86
|
+
if payment["errors"].blank?
|
87
|
+
log UI.fmt("{{v}} NFT mint payment paid: #{payment["data"]}")
|
88
|
+
data["_mint"]["token_id"] = token_id
|
89
|
+
end
|
90
|
+
|
91
|
+
log UI.fmt("{{v}} NFT successfully minted")
|
92
|
+
ensure
|
93
|
+
if File.file? raw
|
94
|
+
File.write raw, data.to_json
|
95
|
+
else
|
96
|
+
log data
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# CLI to transfer to Trident MTG
|
8
|
+
class Order < Base
|
9
|
+
EXCHANGE_ASSET_ID = "965e5c6e-434c-3fa9-b780-c50f43cd955c"
|
10
|
+
MINIMUM_AMOUNT = 0.000_000_01
|
11
|
+
|
12
|
+
desc "index", "list orders"
|
13
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
14
|
+
option :collection, type: :string, aliases: "c", required: false, desc: "collection ID"
|
15
|
+
option :metahash, type: :string, aliases: "m", required: false, desc: "metahash"
|
16
|
+
option :type, type: :string, aliases: "t", required: false, desc: "ask | bid | auction"
|
17
|
+
option :state, type: :string, aliases: "s", required: false, desc: "open | completed"
|
18
|
+
def index
|
19
|
+
log api.orders(
|
20
|
+
collection_id: options[:collection],
|
21
|
+
metahash: options[:metahash],
|
22
|
+
state: options[:state],
|
23
|
+
type: options[:type]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "show ID", "query order"
|
28
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
29
|
+
def show(id)
|
30
|
+
log api.order id
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "sell", "sell NFT at fixed price"
|
34
|
+
def ask; end
|
35
|
+
|
36
|
+
desc "auction", "auction NFT"
|
37
|
+
def auction; end
|
38
|
+
|
39
|
+
desc "bid", "bid NFT"
|
40
|
+
def bid; end
|
41
|
+
|
42
|
+
desc "fill ID", "fill order"
|
43
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
44
|
+
def fill(id)
|
45
|
+
log api.fill_order id
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "cancel ID", "cancel order"
|
49
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
50
|
+
def cancel(id)
|
51
|
+
log api.cancel_order id
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "deposit TOKEN", "deposit NFT"
|
55
|
+
def deposit(token)
|
56
|
+
log api.deposit_nft token
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "withdraw TOKEN", "withdraw NFT"
|
60
|
+
option :keystore, type: :string, aliases: "k", required: true, desc: "keystore or keystore.json file of Mixin bot"
|
61
|
+
def withdraw(token)
|
62
|
+
payment =
|
63
|
+
api.mixin_bot.create_multisig_transaction(
|
64
|
+
keystore[:pin],
|
65
|
+
asset_id: EXCHANGE_ASSET_ID,
|
66
|
+
amount: MINIMUM_AMOUNT,
|
67
|
+
receivers: TridentAssistant::Utils::TRIDENT_MTG[:members],
|
68
|
+
threshold: TridentAssistant::Utils::TRIDENT_MTG[:threshold],
|
69
|
+
memo: TridentAssistant::Utils::Memo.new(type: "W", token_id: token).encode,
|
70
|
+
trace_id: SecureRandom.uuid
|
71
|
+
)
|
72
|
+
|
73
|
+
log UI.fmt("{{v}} payment: #{payment}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
|
5
|
+
module TridentAssistant
|
6
|
+
module CLI
|
7
|
+
# CLI for utils
|
8
|
+
class Utils < Base
|
9
|
+
desc "hash INPUT", "Hash a string or file using sha256"
|
10
|
+
option :file, type: :boolean, aliases: "f", default: false, desc: "Hash a file"
|
11
|
+
def hash(_input)
|
12
|
+
content =
|
13
|
+
if options[:file]
|
14
|
+
File.read options[:input]
|
15
|
+
else
|
16
|
+
options[:input].to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
log SHA3::Digest::SHA256.hexdigest(content)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|