honeymaker 0.12.3 → 0.12.4
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 +4 -4
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +19 -4
- data/Cargo.lock +1078 -0
- data/Cargo.toml +14 -0
- data/README.md +39 -0
- data/Rakefile +28 -16
- data/crates/honeymaker/Cargo.toml +18 -0
- data/crates/honeymaker/src/encode.rs +106 -0
- data/crates/honeymaker/src/kraken/lookup.rs +140 -0
- data/crates/honeymaker/src/kraken/mod.rs +5 -0
- data/crates/honeymaker/src/kraken/normalize.rs +536 -0
- data/crates/honeymaker/src/kraken/requests.rs +286 -0
- data/crates/honeymaker/src/kraken/sign.rs +166 -0
- data/crates/honeymaker/src/kraken/trades.rs +201 -0
- data/crates/honeymaker/src/lib.rs +10 -0
- data/crates/honeymaker/src/num.rs +115 -0
- data/crates/honeymaker/src/semantics.rs +100 -0
- data/crates/honeymaker/src/types.rs +85 -0
- data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
- data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
- data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/crates/honeymaker-client/Cargo.toml +30 -0
- data/crates/honeymaker-client/examples/probe.rs +147 -0
- data/crates/honeymaker-client/src/kraken.rs +514 -0
- data/crates/honeymaker-client/src/lib.rs +4 -0
- data/crates/honeymaker-client/src/transport/connect.rs +583 -0
- data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
- data/crates/honeymaker-client/src/transport/response.rs +169 -0
- data/crates/honeymaker-client/src/transport.rs +467 -0
- data/crates/honeymaker-client/tests/classification.rs +203 -0
- data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
- data/crates/honeymaker-client/tests/response.rs +522 -0
- data/crates/honeymaker-client/tests/support/mod.rs +550 -0
- data/crates/honeymaker-client/tests/transport.rs +828 -0
- data/ext/honeymaker_native/Cargo.toml +16 -0
- data/ext/honeymaker_native/extconf.rb +6 -0
- data/ext/honeymaker_native/src/convert.rs +150 -0
- data/ext/honeymaker_native/src/kraken.rs +362 -0
- data/ext/honeymaker_native/src/lib.rs +90 -0
- data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
- data/lib/honeymaker/clients/bybit.rb +19 -2
- data/lib/honeymaker/clients/coinbase.rb +3 -2
- data/lib/honeymaker/clients/kraken.rb +6 -1
- data/lib/honeymaker/exchanges/coinbase.rb +5 -4
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/version.rb +1 -1
- data/lib/honeymaker.rb +12 -2
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/test/fixtures/coinbase_products.json +1 -1
- data/test/honeymaker/clients/bybit_client_test.rb +25 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
- data/test/honeymaker/clients/kraken_client_test.rb +15 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- metadata +84 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Push built gems: platform gems first, the source gem last. Idempotent: a gem already on
|
|
3
|
+
# rubygems.org is accepted only if byte-identical (a rerun after a partial publish).
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
dir="${1:?gem dir}"
|
|
6
|
+
version="${2:?version}"
|
|
7
|
+
api="${RUBYGEMS_API:-https://rubygems.org}"
|
|
8
|
+
# Exactly the release set, or nothing is pushed: 5 platform gems + the source gem.
|
|
9
|
+
expected=(x86_64-linux aarch64-linux x86_64-darwin arm64-darwin x64-mingw-ucrt)
|
|
10
|
+
for plat in "${expected[@]}"; do
|
|
11
|
+
[ -f "$dir/honeymaker-$version-$plat.gem" ] || { echo "::error::missing honeymaker-$version-$plat.gem" >&2; exit 1; }
|
|
12
|
+
done
|
|
13
|
+
[ -f "$dir/honeymaker-$version.gem" ] || { echo "::error::missing honeymaker-$version.gem (source)" >&2; exit 1; }
|
|
14
|
+
count=$(find "$dir" -maxdepth 1 -name '*.gem' | wc -l | tr -d ' ')
|
|
15
|
+
[ "$count" = 6 ] || { echo "::error::expected 6 gems in $dir, found $count" >&2; exit 1; }
|
|
16
|
+
push() {
|
|
17
|
+
local g="$1" ver plat sha remote
|
|
18
|
+
if gem push "$g"; then return 0; fi
|
|
19
|
+
ver=$(ruby -rrubygems/package -e 'print Gem::Package.new(ARGV[0]).spec.version' "$g")
|
|
20
|
+
plat=$(ruby -rrubygems/package -e 'print Gem::Package.new(ARGV[0]).spec.platform' "$g")
|
|
21
|
+
sha=$(ruby -rdigest -e 'print Digest::SHA256.file(ARGV[0]).hexdigest' "$g")
|
|
22
|
+
remote=$(curl -fsS "$api/api/v2/rubygems/honeymaker/versions/${ver}.json?platform=${plat}" \
|
|
23
|
+
| ruby -rjson -e 'print JSON.parse($stdin.read)["sha"]')
|
|
24
|
+
if [ "$sha" != "$remote" ]; then echo "::error::$g differs from the published gem" >&2; return 1; fi
|
|
25
|
+
echo "$g already published with the same checksum"
|
|
26
|
+
}
|
|
27
|
+
for plat in "${expected[@]}"; do push "$dir/honeymaker-$version-$plat.gem"; done
|
|
28
|
+
push "$dir/honeymaker-$version.gem"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Regenerates crates/honeymaker/tests/vectors/*.json from the LEGACY Ruby implementation.
|
|
4
|
+
# bundle exec ruby -Ilib script/vectors/kraken.rb
|
|
5
|
+
require "honeymaker"
|
|
6
|
+
require "json"
|
|
7
|
+
require "bigdecimal"
|
|
8
|
+
|
|
9
|
+
out_dir = File.expand_path("../../crates/honeymaker/tests/vectors", __dir__)
|
|
10
|
+
|
|
11
|
+
secrets = [
|
|
12
|
+
Base64.strict_encode64("test_secret_key_1234567890123456"),
|
|
13
|
+
"dGVzd A==\n!!-_x", "abc", "=abc", "", " ", "a", "ab==cd", "not base64 at all ~~~",
|
|
14
|
+
Base64.strict_encode64((0..255).to_a.pack("C*"))
|
|
15
|
+
]
|
|
16
|
+
bodies = [
|
|
17
|
+
[["nonce", "1727000000000001"]],
|
|
18
|
+
[["nonce", "1727000000000002"], ["ordertype", "market"], ["type", "buy"], ["volume", "0.001"], ["pair", "XBTUSDT"],
|
|
19
|
+
["oflags", "viqc"], ["close[ordertype]", "limit"]],
|
|
20
|
+
[["nonce", "1727000000000003"], ["txid", "O1,O2"], ["consolidate_taker", "true"]],
|
|
21
|
+
[["nonce", "1727000000000004"], ["asset", "a b~*-._/,:[]\u00e9+&="]]
|
|
22
|
+
]
|
|
23
|
+
paths = %w[/0/private/BalanceEx /0/private/AddOrder /0/private/QueryOrders /0/private/Earn/Allocations]
|
|
24
|
+
|
|
25
|
+
signing = []
|
|
26
|
+
secrets.each do |secret|
|
|
27
|
+
[["key", secret], ["", secret], ["key", ""]].each do |key, sec|
|
|
28
|
+
bodies.each_with_index do |pairs, i|
|
|
29
|
+
body = URI.encode_www_form(pairs)
|
|
30
|
+
c = Honeymaker::Clients::Kraken.new(api_key: key, api_secret: sec)
|
|
31
|
+
signing << { api_key: key, api_secret: sec, path: paths[i], pairs: pairs, body: body,
|
|
32
|
+
decoded_secret_hex: Base64.decode64(sec).unpack1("H*"),
|
|
33
|
+
headers: c.send(:private_headers, paths[i], body).transform_keys(&:to_s) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
decimal_inputs = ["0.5", "1", "-0", "-0.0000", "0.0000000001", "123456789012345678901234567890.123456789",
|
|
39
|
+
"1e-9", "1E5", " 1.5 ", "1_000.5", ".5", "5.", "+2", "", "abc", "1,5", "0x10", "NaN",
|
|
40
|
+
"Infinity", "-Infinity", "1d2", "true"]
|
|
41
|
+
decimals = decimal_inputs.map do |s|
|
|
42
|
+
v = BigDecimal(s)
|
|
43
|
+
{ input: s, ok: true, finite: v.finite?, plain: v.finite? ? v.to_s("F") : v.to_s, sign: v.sign }
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
{ input: s, ok: false, error: e.message }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
File.write(File.join(out_dir, "kraken_signing.json"), JSON.pretty_generate(signing))
|
|
49
|
+
File.write(File.join(out_dir, "decimal.json"), JSON.pretty_generate(decimals))
|
|
50
|
+
puts "wrote #{signing.size} signing vectors, #{decimals.size} decimals"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Regenerates the stage-2 vectors from the LEGACY Ruby implementation.
|
|
4
|
+
# bundle exec ruby -Ilib script/vectors/kraken_stage2.rb
|
|
5
|
+
require "honeymaker"
|
|
6
|
+
require "json"
|
|
7
|
+
require "faraday"
|
|
8
|
+
|
|
9
|
+
OUT = File.expand_path("../../crates/honeymaker/tests/vectors", __dir__)
|
|
10
|
+
SECRET = Base64.strict_encode64("test_secret_key_1234567890123456")
|
|
11
|
+
NONCE = 1_727_000_000_000_001
|
|
12
|
+
|
|
13
|
+
def legacy
|
|
14
|
+
c = Honeymaker::Clients::Kraken.new(api_key: "key", api_secret: SECRET)
|
|
15
|
+
c.define_singleton_method(:nonce) { NONCE }
|
|
16
|
+
c
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# --- A: AddOrder with cl_ord_id and deadline, as legacy signs and sends it ---
|
|
20
|
+
orders = [
|
|
21
|
+
{ ordertype: "market", type: "buy", volume: "0.0012", pair: "XBTEUR", oflags: ["viqc"],
|
|
22
|
+
cl_ord_id: "6f1c1a52-7c8e-4d0e-9a57-0b6f0f1d2e3a", deadline: "2026-09-30T12:00:10.000Z" },
|
|
23
|
+
{ ordertype: "limit", type: "buy", volume: "0.00119", pair: "XBTEUR", price: "49870.1", oflags: [],
|
|
24
|
+
cl_ord_id: "0d9e2c1b-2f4a-4b8c-8d1e-5a6b7c8d9e0f", deadline: "2026-09-30T12:00:10.123Z" },
|
|
25
|
+
{ ordertype: "market", type: "buy", volume: "60", pair: "XBTEUR", oflags: ["viqc"],
|
|
26
|
+
cl_ord_id: "6f1c1a52-7c8e-4d0e-9a57-0b6f0f1d2e3a", deadline: "2026-09-30T12:00:10.000Z", validate: true }
|
|
27
|
+
]
|
|
28
|
+
wire = orders.map do |args|
|
|
29
|
+
body = nil
|
|
30
|
+
stubs = Faraday::Adapter::Test::Stubs.new do |s|
|
|
31
|
+
s.post("/0/private/AddOrder") do |env|
|
|
32
|
+
body = env.body
|
|
33
|
+
[200, { "Content-Type" => "application/json" }, '{"error":[],"result":{"txid":["OTX-1"]}}']
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
c = legacy
|
|
37
|
+
c.instance_variable_set(:@connection, Faraday.new(url: Honeymaker::Clients::Kraken::URL) { |f| f.response :json; f.adapter :test, stubs })
|
|
38
|
+
raise "legacy did not answer" unless c.add_order(**args).success?
|
|
39
|
+
|
|
40
|
+
{ args: args, nonce: NONCE, api_key: "key", api_secret: SECRET, body: body,
|
|
41
|
+
headers: c.send(:private_headers, "/0/private/AddOrder", body).transform_keys(&:to_s) }
|
|
42
|
+
end
|
|
43
|
+
File.write(File.join(OUT, "kraken_add_order_wire.json"), JSON.pretty_generate(wire))
|
|
44
|
+
puts "wrote #{wire.size} AddOrder wire vectors"
|
|
45
|
+
|
|
46
|
+
# --- B: closed_orders_from_trades, the verbatim Ruby loop (with Task 8's dedupe), over scripted
|
|
47
|
+
# pages. Each case says what the STRICT Rust scan must do: "same" as Ruby (a completed scan, a
|
|
48
|
+
# refusal, an unreadable page or a Ruby exception), "incomplete" where Ruby stopped early or
|
|
49
|
+
# trusted a malformed container and returned what it had, or "error" where Ruby skipped a malformed
|
|
50
|
+
# record that Rust must refuse. The label is what Rust must do, not what Ruby did: some
|
|
51
|
+
# "incomplete" cases are Ruby raises (NoMethodError on trades as a non-empty array, a non-empty
|
|
52
|
+
# string or a number, and on count true or an array), not partial answers. Their "outcome" says so.
|
|
53
|
+
def page(trades, count = :none)
|
|
54
|
+
result = { "trades" => trades }
|
|
55
|
+
result["count"] = count unless count == :none
|
|
56
|
+
{ "error" => [], "result" => result }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def trade(o, vol, cost, type: "buy", ordertype: "market", fee: "0.1")
|
|
60
|
+
{ "ordertxid" => o, "vol" => vol, "cost" => cost, "fee" => fee, "type" => type, "ordertype" => ordertype, "time" => 1.5 }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
T = ->(i, o = "O1") { { "T#{i}" => trade(o, "1", "2") } }
|
|
64
|
+
cases = [
|
|
65
|
+
["one page", :same, %w[O1], 1_700_000_000, 20, [page({ "T1" => trade("O1", "0.0006", "30"), "T2" => trade("O1", "0.0006", "30") }, 2)]],
|
|
66
|
+
["partial fill across pages, unwanted skipped", :same, %w[O1 O2], 1_700_000_000, 20,
|
|
67
|
+
[page({ "T1" => trade("O1", "1", "2"), "T2" => trade("O9", "1", "2") }, 3),
|
|
68
|
+
page({ "T3" => trade("O1", "1", "2"), "T4" => trade("O2", "2", "8", ordertype: "limit", type: "sell") }, 3)]],
|
|
69
|
+
["no wanted ids: no request", :same, [], nil, 20, []],
|
|
70
|
+
["a trade on two pages: rows reach count, distinct trades do not", :incomplete, %w[O1], nil, 20,
|
|
71
|
+
[page({ "T1" => trade("O1", "1", "2"), "T2" => trade("O1", "3", "9") }, 3), page({ "T2" => trade("O1", "3", "9") }, 3)]],
|
|
72
|
+
["overlapping pages that cover every trade", :same, %w[O1], nil, 20,
|
|
73
|
+
[page({ "T1" => trade("O1", "1", "2"), "T2" => trade("O1", "3", "9") }, 3),
|
|
74
|
+
page({ "T2" => trade("O1", "3", "9"), "T3" => trade("O1", "1", "1") }, 3)]],
|
|
75
|
+
["complete exactly at the page cap", :same, %w[O1], nil, 2, [page(T[1], 2), page(T[2], 2)]],
|
|
76
|
+
["an empty page after the last trade", :same, %w[O1], nil, 20, [page({}, 0)]],
|
|
77
|
+
["a fill beyond the page cap", :incomplete, %w[O1], nil, 2, [page(T[1, "O9"], 3), page(T[2, "O9"], 3), page(T[3], 3)]],
|
|
78
|
+
["count as a string", :incomplete, %w[O1], nil, 20, [page(T[1], "2"), page(T[2], "2")]],
|
|
79
|
+
["count 1_000 pages to max_pages", :incomplete, %w[O1], nil, 2, [page(T[1], "1_000"), page(T[2], "1_000"), page(T[3], "1_000")]],
|
|
80
|
+
["bignum string count", :incomplete, %w[O1], nil, 3, [page(T[1], "99999999999999999999999"), page(T[2], "1"), page(T[3], "1")]],
|
|
81
|
+
["bignum integer count", :incomplete, %w[O1], nil, 3, [page(T[1], 10**25), page(T[2], 10**25), page(T[3], 10**25), page(T[4], 10**25)]],
|
|
82
|
+
["float count", :incomplete, %w[O1], nil, 20, [page(T[1], 2.9), page(T[2], 2.9), page(T[3], 2.9)]],
|
|
83
|
+
["missing count", :incomplete, %w[O1], nil, 20, [page(T[1]), page(T[2])]],
|
|
84
|
+
["nil count", :incomplete, %w[O1], nil, 20, [page(T[1], nil), page(T[2])]],
|
|
85
|
+
["negative count", :incomplete, %w[O1], nil, 20, [page(T[1], -1), page(T[2])]],
|
|
86
|
+
*[" 3", "3abc", "0x10", "_3", "3__0", "+3", "-3", "", "abc"].map do |c|
|
|
87
|
+
["count #{c.inspect}", :incomplete, %w[O1], nil, 4, (1..4).map { |i| page(T[i], c) }]
|
|
88
|
+
end,
|
|
89
|
+
["empty trades before count", :incomplete, %w[O1], nil, 20, [page({}, 5), page(T[1], 5)]],
|
|
90
|
+
["missing trades", :incomplete, %w[O1], nil, 20, [{ "error" => [], "result" => { "count" => 3 } }, page(T[1])]],
|
|
91
|
+
["trades false", :incomplete, %w[O1], nil, 20, [page(false, 3), page(T[1])]],
|
|
92
|
+
["trades []", :incomplete, %w[O1], nil, 20, [page([], 3), page(T[1])]],
|
|
93
|
+
["trades \"\"", :incomplete, %w[O1], nil, 20, [page("", 3), page(T[1])]],
|
|
94
|
+
["nil result", :incomplete, %w[O1], nil, 20, [{ "error" => [] }, page(T[1])]],
|
|
95
|
+
["result as a string raises", :same, %w[O1], nil, 20, [{ "error" => [], "result" => "x" }]],
|
|
96
|
+
["result as an array raises", :same, %w[O1], nil, 20, [{ "error" => [], "result" => [1] }]],
|
|
97
|
+
["result false raises", :same, %w[O1], nil, 20, [{ "error" => [], "result" => false }]],
|
|
98
|
+
["trades as a non-empty array", :incomplete, %w[O1], nil, 20, [page([1], 1)]],
|
|
99
|
+
["trades as a non-empty string", :incomplete, %w[O1], nil, 20, [page("x", 1)]],
|
|
100
|
+
["trades as a number", :incomplete, %w[O1], nil, 20, [page(5, 1)]],
|
|
101
|
+
*[[false, "false"], [[], "[]"], ["", "\"\""], [nil, "null"]].map do |trades, label|
|
|
102
|
+
["trades #{label} with count 0", :incomplete, %w[O1], nil, 20, [page(trades, 0)]]
|
|
103
|
+
end,
|
|
104
|
+
["trades missing with count 0", :incomplete, %w[O1], nil, 20, [{ "error" => [], "result" => { "count" => 0 } }]],
|
|
105
|
+
["a trade that is a string", :error, %w[O1], nil, 20, [page({ "T1" => "no id here", "T2" => trade("O1", "1", "2") }, 2)]],
|
|
106
|
+
["a record without ordertxid", :error, %w[O1], nil, 20, [page({ "T1" => { "vol" => "1", "cost" => "2" } }, 1)]],
|
|
107
|
+
["a record with a numeric ordertxid", :error, %w[O1], nil, 20, [page({ "T1" => trade(5, "1", "2") }, 1)]],
|
|
108
|
+
["a trade that is a number raises", :same, %w[O1], nil, 20, [page({ "T1" => 5 }, 1)]],
|
|
109
|
+
["a trade that is null raises", :same, %w[O1], nil, 20, [page({ "T1" => nil }, 1)]],
|
|
110
|
+
["count true", :incomplete, %w[O1], nil, 20, [page(T[1], true)]],
|
|
111
|
+
["count as an array", :incomplete, %w[O1], nil, 20, [page(T[1], [1])]],
|
|
112
|
+
["refusal on page 2", :same, %w[O1], nil, 20, [page(T[1], 5), { "error" => ["EAPI:Rate limit exceeded"] }]],
|
|
113
|
+
["falsy errors are not a refusal", :same, %w[O1], nil, 20, [{ "error" => [nil, false], "result" => { "trades" => T[1], "count" => 1 } }]],
|
|
114
|
+
["non-object page 2 is unreadable", :same, %w[O1], nil, 20, [page(T[1], 5), "oops"]],
|
|
115
|
+
["pages run out: legacy sees null", :same, %w[O1], nil, 20, [page(T[1], 5)]],
|
|
116
|
+
["zero volume has no price", :same, %w[O1], nil, 20, [page({ "T1" => trade("O1", "0", "0") }, 1)]]
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
pager = cases.map do |name, strict, ids, start, max_pages, pages|
|
|
120
|
+
texts = pages.map { |p| JSON.generate(p) }
|
|
121
|
+
queue = texts.dup
|
|
122
|
+
requests = []
|
|
123
|
+
c = legacy
|
|
124
|
+
c.define_singleton_method(:get_trades_history) do |start: nil, ofs: nil, **|
|
|
125
|
+
requests << [start, ofs]
|
|
126
|
+
Honeymaker::Result::Success.new(JSON.parse(queue.shift || "null"))
|
|
127
|
+
end
|
|
128
|
+
outcome = begin
|
|
129
|
+
r = c.closed_orders_from_trades(order_ids: ids, start: start, max_pages: max_pages)
|
|
130
|
+
if r.success?
|
|
131
|
+
{ "ok" => r.data.map do |txid, a|
|
|
132
|
+
[txid, { "vol" => a[:amount_exec].to_s("F"), "cost" => a[:quote_amount_exec].to_s("F"), "fee" => a[:fee].to_s("F"),
|
|
133
|
+
"price" => a[:price]&.to_s("F"), "side" => a[:side]&.to_s, "order_type" => a[:order_type].to_s }]
|
|
134
|
+
end }
|
|
135
|
+
elsif r.data.is_a?(Hash) && r.data[:unreadable]
|
|
136
|
+
{ "unreadable" => true }
|
|
137
|
+
else
|
|
138
|
+
{ "venue" => r.errors }
|
|
139
|
+
end
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
{ "raise" => e.class.name }
|
|
142
|
+
end
|
|
143
|
+
{ name: name, strict: strict, order_ids: ids, start: start, max_pages: max_pages, pages: texts, requests: requests, outcome: outcome }
|
|
144
|
+
end
|
|
145
|
+
File.write(File.join(OUT, "kraken_trades_pager.json"), JSON.pretty_generate(pager))
|
|
146
|
+
puts "wrote #{pager.size} trades-pager vectors"
|
|
@@ -46,6 +46,31 @@ class Honeymaker::Clients::BybitTest < Minitest::Test
|
|
|
46
46
|
assert_equal :closed, result.data[:status]
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def test_get_order_partially_filled_canceled_is_cancelled
|
|
50
|
+
stub_connection(:get, { "retCode" => 0, "retMsg" => "OK", "result" => { "list" => [{
|
|
51
|
+
"orderId" => "123", "orderType" => "Limit", "side" => "Buy", "orderStatus" => "PartiallyFilledCanceled",
|
|
52
|
+
"avgPrice" => "50000", "qty" => "0.002", "cumExecQty" => "0.001", "cumExecValue" => "50"
|
|
53
|
+
}] } })
|
|
54
|
+
result = @client.get_order(category: "spot", order_id: "123")
|
|
55
|
+
assert_equal :cancelled, result.data[:status]
|
|
56
|
+
assert_equal BigDecimal("0.001"), result.data[:amount_exec]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_order_history
|
|
60
|
+
@client.expects(:get_authenticated).with("/v5/order/history", {
|
|
61
|
+
category: "spot", symbol: nil, orderId: "123", orderLinkId: nil, orderStatus: nil,
|
|
62
|
+
startTime: 1, endTime: 2, limit: 50, cursor: "c"
|
|
63
|
+
}).returns(Honeymaker::Result::Success.new({ "retCode" => 0 }))
|
|
64
|
+
assert @client.order_history(category: "spot", order_id: "123", start_time: 1, end_time: 2, limit: 50, cursor: "c").success?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_internal_deposit_records
|
|
68
|
+
@client.expects(:get_authenticated).with("/v5/asset/deposit/query-internal-record", {
|
|
69
|
+
txID: nil, coin: "USDT", startTime: 1, endTime: 2, limit: 50, cursor: "c"
|
|
70
|
+
}).returns(Honeymaker::Result::Success.new({ "retCode" => 0 }))
|
|
71
|
+
assert @client.internal_deposit_records(coin: "USDT", start_time: 1, end_time: 2, limit: 50, cursor: "c").success?
|
|
72
|
+
end
|
|
73
|
+
|
|
49
74
|
def test_cancel_order
|
|
50
75
|
stub_connection(:post, { "result" => { "orderId" => "123" } })
|
|
51
76
|
result = @client.cancel_order(category: "spot", symbol: "BTCUSDT", order_id: "123")
|
|
@@ -69,6 +69,20 @@ class Honeymaker::Clients::CoinbaseTest < Minitest::Test
|
|
|
69
69
|
assert result.success?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def test_list_accounts_passes_cursor_and_limit
|
|
73
|
+
response = stub(body: { "accounts" => [], "has_next" => false, "cursor" => "" })
|
|
74
|
+
request = stub(url: nil)
|
|
75
|
+
request.stubs(:headers=)
|
|
76
|
+
request.stubs(:http_method).returns(:get)
|
|
77
|
+
request.stubs(:path).returns("/api/v3/brokerage/accounts")
|
|
78
|
+
request.expects(:params=).with({ limit: 250, cursor: "c2" })
|
|
79
|
+
connection = stub
|
|
80
|
+
connection.stubs(:get).yields(request).returns(response)
|
|
81
|
+
@client.instance_variable_set(:@connection, connection)
|
|
82
|
+
|
|
83
|
+
assert @client.list_accounts(limit: 250, cursor: "c2").success?
|
|
84
|
+
end
|
|
85
|
+
|
|
72
86
|
def test_send_money
|
|
73
87
|
stub_connection(:post, { "data" => { "id" => "tx1" } })
|
|
74
88
|
result = @client.send_money(account_id: "acc1", to: "addr", amount: "0.1", currency: "BTC")
|
|
@@ -223,6 +223,21 @@ class Honeymaker::Clients::KrakenTest < Minitest::Test
|
|
|
223
223
|
assert_equal BigDecimal("0.00016557"), agg[:amount_exec]
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
# A trade that lands at the top of TradesHistory between two page reads pushes TID-2 from page 1
|
|
227
|
+
# onto page 2. Offset paging then serves TID-2 twice; it must be counted once.
|
|
228
|
+
def test_closed_orders_from_trades_counts_a_trade_seen_on_two_pages_once
|
|
229
|
+
t = { "ordertxid" => "OABC", "pair" => "XXBTZUSD", "type" => "buy", "ordertype" => "market",
|
|
230
|
+
"cost" => "6.0", "vol" => "0.0001", "fee" => "0.01", "time" => 1_700_000_000.0 }
|
|
231
|
+
p1 = { "error" => [], "result" => { "count" => 3, "trades" => { "TID-1" => t, "TID-2" => t } } }
|
|
232
|
+
p2 = { "error" => [], "result" => { "count" => 3, "trades" => { "TID-2" => t } } }
|
|
233
|
+
@client.stubs(:get_trades_history).returns(Honeymaker::Result::Success.new(p1), Honeymaker::Result::Success.new(p2))
|
|
234
|
+
agg = @client.closed_orders_from_trades(order_ids: ["OABC"], start: 1).data["OABC"]
|
|
235
|
+
assert_equal BigDecimal("0.0002"), agg[:amount_exec]
|
|
236
|
+
assert_equal BigDecimal("12.0"), agg[:quote_amount_exec]
|
|
237
|
+
assert_equal BigDecimal("0.02"), agg[:fee]
|
|
238
|
+
assert_equal 2, agg[:trade_count]
|
|
239
|
+
end
|
|
240
|
+
|
|
226
241
|
private
|
|
227
242
|
|
|
228
243
|
def assert_strictly_increasing(values)
|
|
@@ -16,7 +16,7 @@ class Honeymaker::Exchanges::CoinbaseTest < Minitest::Test
|
|
|
16
16
|
result = @exchange.get_tickers_info
|
|
17
17
|
|
|
18
18
|
assert result.success?
|
|
19
|
-
#
|
|
19
|
+
# RNDR is blacklisted, so only BTC-USD should remain
|
|
20
20
|
assert_equal 1, result.data.size
|
|
21
21
|
ticker = result.data.first
|
|
22
22
|
assert_equal "BTC-USD", ticker[:ticker]
|
|
@@ -54,6 +54,26 @@ class Honeymaker::Exchanges::CoinbaseTest < Minitest::Test
|
|
|
54
54
|
refute result.data.first[:trading_enabled]
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def test_get_tickers_info_disables_restricted_modes
|
|
58
|
+
%w[limit_only cancel_only is_disabled].each do |flag|
|
|
59
|
+
body = load_fixture("coinbase_products.json")
|
|
60
|
+
body["products"].first[flag] = true
|
|
61
|
+
stub_connection(body)
|
|
62
|
+
|
|
63
|
+
ticker = @exchange.get_tickers_info.data.first
|
|
64
|
+
assert ticker[:available], flag
|
|
65
|
+
refute ticker[:trading_enabled], flag
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_keeps_render
|
|
70
|
+
body = load_fixture("coinbase_products.json")
|
|
71
|
+
body["products"] << body["products"].last.merge("product_id" => "RENDER-USD")
|
|
72
|
+
stub_connection(body)
|
|
73
|
+
|
|
74
|
+
assert_includes @exchange.get_tickers_info.data.map { |t| t[:base] }, "RENDER"
|
|
75
|
+
end
|
|
76
|
+
|
|
57
77
|
def test_filters_blacklisted_assets
|
|
58
78
|
body = load_fixture("coinbase_products.json")
|
|
59
79
|
stub_connection(body)
|
|
@@ -61,7 +81,7 @@ class Honeymaker::Exchanges::CoinbaseTest < Minitest::Test
|
|
|
61
81
|
result = @exchange.get_tickers_info
|
|
62
82
|
|
|
63
83
|
bases = result.data.map { |t| t[:base] }
|
|
64
|
-
refute_includes bases, "
|
|
84
|
+
refute_includes bases, "RNDR"
|
|
65
85
|
end
|
|
66
86
|
|
|
67
87
|
def test_get_bid_ask_parses_response
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
class Honeymaker::Native::BackendSwitchTest < Minitest::Test
|
|
7
|
+
LIB = File.expand_path("../../lib", __dir__)
|
|
8
|
+
|
|
9
|
+
def ruby(env, code)
|
|
10
|
+
out, status = Open3.capture2e(env, RbConfig.ruby, "-I#{LIB}", "-rbundler/setup", "-e", code)
|
|
11
|
+
[out.strip, status]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_flag_off_uses_ruby_and_never_loads_the_extension
|
|
15
|
+
out, st = ruby({ "HONEYMAKER_NATIVE" => nil }, <<~RUBY)
|
|
16
|
+
require "honeymaker"
|
|
17
|
+
print [Honeymaker::CLIENTS["kraken"], Honeymaker::EXCHANGES["kraken"], Honeymaker.backend("kraken"),
|
|
18
|
+
$LOADED_FEATURES.grep(/honeymaker_native\\.(so|bundle|dll)\\z/).size].join(",")
|
|
19
|
+
RUBY
|
|
20
|
+
assert st.success?, out
|
|
21
|
+
assert_equal "Honeymaker::Clients::Kraken,Honeymaker::Exchanges::Kraken,ruby,0", out
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_unsupported_flags_fail_at_require_time
|
|
25
|
+
["binance", " kraken, ,binance ", " KRAKEN, bybit "].each do |flag|
|
|
26
|
+
out, st = ruby({ "HONEYMAKER_NATIVE" => flag }, 'require "honeymaker"')
|
|
27
|
+
refute st.success?, out
|
|
28
|
+
assert_match(/unsupported.*(?:binance|bybit)/i, out)
|
|
29
|
+
assert_match(/supported.*kraken/i, out)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_backend_only_reports_supported_enabled_names
|
|
34
|
+
out, st = ruby({ "HONEYMAKER_NATIVE" => " KRAKEN, ,kraken " }, <<~RUBY)
|
|
35
|
+
require "honeymaker"
|
|
36
|
+
print [Honeymaker.backend(" KRAKEN "), Honeymaker.backend(:binance), Honeymaker.backend("unknown")].join(",")
|
|
37
|
+
RUBY
|
|
38
|
+
assert st.success?, out
|
|
39
|
+
assert_equal "native,ruby,ruby", out
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_flag_on_selects_native_for_kraken_only
|
|
43
|
+
skip "native extension not compiled" unless Dir[File.join(LIB, "honeymaker", "**", "honeymaker_native.{so,bundle,dll}")].any?
|
|
44
|
+
|
|
45
|
+
out, st = ruby({ "HONEYMAKER_NATIVE" => "kraken" }, <<~RUBY)
|
|
46
|
+
require "honeymaker"
|
|
47
|
+
print [Honeymaker.client("kraken").class, Honeymaker.exchange("kraken").class, Honeymaker.backend(:kraken),
|
|
48
|
+
Honeymaker.client("binance").class, $LOADED_FEATURES.grep(/honeymaker_native\\.(so|bundle|dll)\\z/).size].join(",")
|
|
49
|
+
RUBY
|
|
50
|
+
assert st.success?, out
|
|
51
|
+
assert_equal "Honeymaker::Native::KrakenClient,Honeymaker::Native::KrakenExchange,native,Honeymaker::Clients::Binance,1", out
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
|
|
6
|
+
class Honeymaker::Native::ConvertTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
Honeymaker::Native.load!
|
|
9
|
+
rescue LoadError
|
|
10
|
+
raise if ENV["HONEYMAKER_REQUIRE_NATIVE"]
|
|
11
|
+
|
|
12
|
+
skip "native extension not compiled"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ext = Honeymaker::Native::Ext
|
|
16
|
+
|
|
17
|
+
def test_numeric_text_matches_ruby_to_s
|
|
18
|
+
[1.0e-09, -0.0, 0.0, 1.0e20, 1688888888.1234, Float::MIN, Float::MAX,
|
|
19
|
+
Float::INFINITY, -Float::INFINITY, Float::NAN, 12345678901234567890123].each do |value|
|
|
20
|
+
assert_equal value.to_s, ext.to_s_for_tests(value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_decimal_json_and_to_s_follow_ruby
|
|
25
|
+
["0.5", 12345678901234567890123, 1.0e-09, -0.0, Float::INFINITY,
|
|
26
|
+
-Float::INFINITY, Float::NAN, nil, true, false, [], { "k" => 1 }].each do |value|
|
|
27
|
+
[true, false].each do |stringify|
|
|
28
|
+
want = (BigDecimal(stringify ? value.to_s : value) rescue $!)
|
|
29
|
+
got = (ext.decimal_convert_for_tests(value, stringify) rescue $!)
|
|
30
|
+
assert_equal want.class, got.class, [value, stringify].inspect
|
|
31
|
+
if want.is_a?(Exception)
|
|
32
|
+
assert_equal want.message, got.message
|
|
33
|
+
else
|
|
34
|
+
assert_equal [want.to_s, want.sign], [got.to_s, got.sign]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_decimal_arithmetic_survives_gc_in_rust_container
|
|
41
|
+
[["1", "3"], ["-0.0", "2"], ["NaN", "Infinity"], ["1", "0"]].each do |a, b|
|
|
42
|
+
left, right = BigDecimal(a), BigDecimal(b)
|
|
43
|
+
want = [left + right, left - right, left / right]
|
|
44
|
+
GC.stress = true
|
|
45
|
+
got = ext.decimal_arithmetic_for_tests(a, b)
|
|
46
|
+
GC.stress = false
|
|
47
|
+
assert_equal want.map { |n| [n.to_s, n.sign] }, got.take(3).map { |n| [n.to_s, n.sign] }
|
|
48
|
+
assert_equal left.zero?, got[3]
|
|
49
|
+
end
|
|
50
|
+
ensure
|
|
51
|
+
GC.stress = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_non_utf8_strings_roundtrip_with_bytes_and_encoding
|
|
55
|
+
["O\xff", "O\xff".b, "é".b, "é".encode("ISO-8859-1")].each do |value|
|
|
56
|
+
got = ext.roundtrip_for_tests([value]).first
|
|
57
|
+
assert_equal value.bytes, got.bytes
|
|
58
|
+
assert_equal value.encoding, got.encoding
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_invalid_string_hash_keys_roundtrip
|
|
63
|
+
["O\xff", "O\xff".b, "é".b].each do |key|
|
|
64
|
+
got = ext.roundtrip_for_tests({ key => "value" }).keys.first
|
|
65
|
+
assert_equal key.bytes, got.bytes
|
|
66
|
+
assert_equal key.encoding, got.encoding
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_internal_string_sentinel_names_are_ordinary_hash_keys
|
|
71
|
+
obj = { "\0ruby_string" => [[], "UTF-8"], "\0ruby_key:\"literal\"" => "value" }
|
|
72
|
+
assert_equal obj, ext.roundtrip_for_tests(obj)
|
|
73
|
+
assert_equal({ "\0ruby_string" => [[], "UTF-8"] }, ext.roundtrip_for_tests({ "\0ruby_string" => [[], "UTF-8"] }))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_invalid_decimal_strings_use_rubys_own_errors
|
|
77
|
+
["1\xff", "1\xff".b].each do |value|
|
|
78
|
+
[true, false].each do |stringify|
|
|
79
|
+
want = (BigDecimal(value) rescue $!)
|
|
80
|
+
got = (ext.decimal_convert_for_tests(value, stringify) rescue $!)
|
|
81
|
+
assert_equal [want.class, want.message], [got.class, got.message]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_shape_errors
|
|
87
|
+
error = assert_raises(Honeymaker::Native::ShapeError) { ext.roundtrip_for_tests({ key: 1 }) }
|
|
88
|
+
assert_equal "non-string key", error.message
|
|
89
|
+
error = assert_raises(Honeymaker::Native::ShapeError) { ext.roundtrip_for_tests(:unsupported) }
|
|
90
|
+
assert_equal "unsupported value :unsupported", error.message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_false_negative_infinity_nan_and_negative_zero
|
|
94
|
+
got = ext.roundtrip_for_tests([false, -Float::INFINITY, Float::NAN, -0.0])
|
|
95
|
+
assert_equal false, got[0]
|
|
96
|
+
assert_equal(-Float::INFINITY, got[1])
|
|
97
|
+
assert_predicate got[2], :nan?
|
|
98
|
+
assert_equal(-Float::INFINITY, 1.0 / got[3])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_json_roundtrip_is_exact
|
|
102
|
+
obj = JSON.parse('{"i":12345678901234567890123,"f":1688888888.1234,"e":1e-09,"z":-0.0,"s":"\u00e9","n":null,' \
|
|
103
|
+
'"b":true,"a":[1,2.5,"x"],"big":1e400,"o":{"k":{"k2":[]}}}')
|
|
104
|
+
got = ext.roundtrip_for_tests(obj)
|
|
105
|
+
assert_equal obj.keys, got.keys
|
|
106
|
+
obj.keys.each do |k|
|
|
107
|
+
v = obj[k]
|
|
108
|
+
assert_equal v.class, got[k].class, k
|
|
109
|
+
assert(v.is_a?(Float) ? v.eql?(got[k]) : v == got[k], k)
|
|
110
|
+
end
|
|
111
|
+
assert_equal Encoding::UTF_8, got["s"].encoding
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_decimal_is_rubys_own
|
|
115
|
+
["0.5", "-0.0000", "NaN", "Infinity", "1_000", "1e-9"].each do |s|
|
|
116
|
+
want = BigDecimal(s)
|
|
117
|
+
got = ext.decimal_for_tests(s)
|
|
118
|
+
assert_equal [want.to_s, want.sign], [got.to_s, got.sign], s
|
|
119
|
+
end
|
|
120
|
+
["", "abc", "true", "[]", "{}"].each do |s|
|
|
121
|
+
want = (BigDecimal(s) rescue $!)
|
|
122
|
+
got = (ext.decimal_for_tests(s) rescue $!)
|
|
123
|
+
assert_equal [want.class, want.message], [got.class, got.message], s.inspect
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_survives_gc_stress_and_compaction
|
|
128
|
+
obj = JSON.parse({ "rows" => (1..2_000).map { |i| { "id" => "O#{i}", "vol" => "0.#{i}", "t" => i + 0.5 } } }.to_json)
|
|
129
|
+
GC.stress = true
|
|
130
|
+
got = ext.roundtrip_for_tests(obj)
|
|
131
|
+
GC.stress = false
|
|
132
|
+
GC.compact if GC.respond_to?(:compact)
|
|
133
|
+
assert_equal obj, got
|
|
134
|
+
ensure
|
|
135
|
+
GC.stress = false
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
# Spec §2.2: the platform gems cross-compile with nothing but pure-Rust crypto. The async client
|
|
7
|
+
# lives in the same Cargo workspace, so this proves the extension never pulls it in.
|
|
8
|
+
class Honeymaker::Native::GemIsolationTest < Minitest::Test
|
|
9
|
+
FORBIDDEN = /\A(tokio|tokio-rustls|hyper|hyper-util|rustls|ring|aws-lc-rs|aws-lc-sys|webpki-roots|honeymaker-client) /
|
|
10
|
+
|
|
11
|
+
def test_the_extension_does_not_depend_on_the_async_client_stack
|
|
12
|
+
out, status = Open3.capture2("cargo", "tree", "--locked", "-p", "honeymaker_native", "-e", "normal,build",
|
|
13
|
+
"--prefix", "none", chdir: File.expand_path("../..", __dir__))
|
|
14
|
+
assert status.success?, "cargo tree failed"
|
|
15
|
+
assert_empty out.lines.grep(FORBIDDEN).uniq
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Native::LoaderTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
Honeymaker::Native.load!
|
|
8
|
+
rescue LoadError => e
|
|
9
|
+
raise if ENV["HONEYMAKER_REQUIRE_NATIVE"]
|
|
10
|
+
|
|
11
|
+
skip "native extension not compiled (#{e.message})"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_extension_reports_the_gem_version
|
|
15
|
+
assert_equal Honeymaker::VERSION, Honeymaker::Native::Ext.version
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_flag_parsing
|
|
19
|
+
Honeymaker::Native.instance_variable_set(:@enabled, nil)
|
|
20
|
+
ENV["HONEYMAKER_NATIVE"] = " KRAKEN, ,kraken "
|
|
21
|
+
assert_equal %w[kraken], Honeymaker::Native.enabled
|
|
22
|
+
assert Honeymaker::Native.enabled?(:kraken)
|
|
23
|
+
assert Honeymaker::Native.enabled?(" KRAKEN ")
|
|
24
|
+
refute Honeymaker::Native.enabled?("bybit")
|
|
25
|
+
ensure
|
|
26
|
+
ENV.delete("HONEYMAKER_NATIVE")
|
|
27
|
+
Honeymaker::Native.instance_variable_set(:@enabled, nil)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
class Honeymaker::Native::LockstepTest < Minitest::Test
|
|
7
|
+
def test_cargo_workspace_version_equals_gem_version
|
|
8
|
+
cargo = File.read(File.expand_path("../../Cargo.toml", __dir__))
|
|
9
|
+
assert_equal Honeymaker::VERSION, cargo[/^\[workspace\.package\].*?^version = "([^"]+)"/m, 1]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_bump_updates_cargo_too
|
|
13
|
+
rakefile = File.expand_path("../../Rakefile", __dir__)
|
|
14
|
+
Dir.mktmpdir do |dir|
|
|
15
|
+
FileUtils.mkdir_p(File.join(dir, "lib/honeymaker"))
|
|
16
|
+
File.write(File.join(dir, "lib/honeymaker/version.rb"), %(module Honeymaker\n VERSION = "1.2.3"\nend\n))
|
|
17
|
+
File.write(File.join(dir, "Cargo.toml"), %([workspace]\nmembers = []\n\n[workspace.package]\nversion = "1.2.3" # c\n))
|
|
18
|
+
out = Dir.chdir(dir) { `HONEYMAKER_RAKEFILE_LIB_ONLY=1 #{RbConfig.ruby} -rrake -e 'load "#{rakefile}"; bump_version(:minor)' 2>&1` }
|
|
19
|
+
assert_includes out, "1.3.0"
|
|
20
|
+
assert_includes File.read(File.join(dir, "Cargo.toml")), %(version = "1.3.0" # c)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class KrakenSigningSmokeTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
Honeymaker::Native.load!
|
|
8
|
+
require_relative "../support/kraken_signing_smoke"
|
|
9
|
+
rescue LoadError => e
|
|
10
|
+
raise if ENV["HONEYMAKER_REQUIRE_NATIVE"]
|
|
11
|
+
|
|
12
|
+
skip e.message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_real_signing_vector
|
|
16
|
+
assert KrakenSigningSmoke.verify!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_rejects_incorrect_key_or_signature
|
|
20
|
+
vector = JSON.parse(File.read(KrakenSigningSmoke::VECTORS)).find { |v| !v.fetch("api_key").empty? && !v.fetch("api_secret").empty? }
|
|
21
|
+
%w[API-Key API-Sign].each do |field|
|
|
22
|
+
signer = stub(sign: vector.fetch("headers").merge(field => "wrong"))
|
|
23
|
+
Honeymaker::Native::Ext::Kraken.stubs(:new).with(vector.fetch("api_key"), vector.fetch("api_secret")).returns(signer)
|
|
24
|
+
_, stderr = capture_io { assert_raises(SystemExit) { KrakenSigningSmoke.verify! } }
|
|
25
|
+
assert_match(/#{field}/, stderr)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|