honeymaker 0.12.3 → 0.12.5
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/bingx.rb +10 -6
- data/lib/honeymaker/clients/bitrue.rb +43 -13
- data/lib/honeymaker/clients/bitvavo.rb +9 -4
- data/lib/honeymaker/clients/bybit.rb +19 -2
- data/lib/honeymaker/clients/coinbase.rb +3 -2
- data/lib/honeymaker/clients/gemini.rb +10 -0
- data/lib/honeymaker/clients/hyperliquid.rb +62 -3
- data/lib/honeymaker/clients/kraken.rb +6 -1
- data/lib/honeymaker/clients/mexc.rb +16 -2
- data/lib/honeymaker/exchanges/coinbase.rb +5 -4
- data/lib/honeymaker/exchanges/mexc.rb +8 -3
- 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/bingx_client_test.rb +19 -6
- data/test/honeymaker/clients/bitrue_client_test.rb +48 -0
- data/test/honeymaker/clients/bitvavo_client_test.rb +15 -0
- data/test/honeymaker/clients/bybit_client_test.rb +25 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
- data/test/honeymaker/clients/gemini_client_test.rb +15 -0
- data/test/honeymaker/clients/hyperliquid_client_test.rb +120 -4
- data/test/honeymaker/clients/kraken_client_test.rb +15 -0
- data/test/honeymaker/clients/mexc_client_test.rb +12 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
- data/test/honeymaker/exchanges/mexc_test.rb +14 -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"
|
|
@@ -83,16 +83,29 @@ class Honeymaker::Clients::BingXTest < Minitest::Test
|
|
|
83
83
|
assert result.success?
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# /openApi/spot/v1/trade/fills and /openApi/wallets/v1/capital/{deposit,withdraw}/... answer
|
|
87
|
+
# 100400 "this api is not exist"; these are the documented paths.
|
|
88
|
+
def test_get_trade_fills_reads_my_trades
|
|
89
|
+
@client.expects(:get_signed).with("/openApi/spot/v1/trade/myTrades",
|
|
90
|
+
{ symbol: "BTC-USDT", orderId: nil, startTime: 1, endTime: 2, fromId: nil, limit: 500 })
|
|
91
|
+
@client.get_trade_fills(symbol: "BTC-USDT", start_time: 1, end_time: 2, limit: 500)
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
def test_deposit_history
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
assert
|
|
95
|
+
@client.expects(:get_signed).with("/openApi/api/v3/capital/deposit/hisrec", has_entries(offset: 0, limit: 1000))
|
|
96
|
+
.returns(Honeymaker::Result::Success.new([{ "coin" => "BTC" }]))
|
|
97
|
+
assert @client.deposit_history(offset: 0, limit: 1000).success?
|
|
90
98
|
end
|
|
91
99
|
|
|
92
100
|
def test_withdraw_history
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
assert
|
|
101
|
+
@client.expects(:get_signed).with("/openApi/api/v3/capital/withdraw/history", has_entries(startTime: 1, endTime: 2))
|
|
102
|
+
.returns(Honeymaker::Result::Success.new([{ "coin" => "ETH" }]))
|
|
103
|
+
assert @client.withdraw_history(start_time: 1, end_time: 2).success?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_get_klines_sends_time_zone
|
|
107
|
+
@client.expects(:get_public).with("/openApi/spot/v1/market/kline", has_entries(symbol: "BTC-USDT", timeZone: 0))
|
|
108
|
+
@client.get_klines(symbol: "BTC-USDT", interval: "1d", time_zone: 0)
|
|
96
109
|
end
|
|
97
110
|
|
|
98
111
|
private
|
|
@@ -53,6 +53,54 @@ class Honeymaker::Clients::BitrueTest < Minitest::Test
|
|
|
53
53
|
assert result.success?
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# The /capital/ paths and /api/v1/myTrades are not in Bitrue's docs; the former redirect to the website.
|
|
57
|
+
def test_account_trade_list_reads_v2
|
|
58
|
+
@client.expects(:get_signed).with("/api/v2/myTrades", has_entries(symbol: "BTCUSDT", startTime: 1, endTime: 2, limit: 1000))
|
|
59
|
+
@client.account_trade_list(symbol: "BTCUSDT", start_time: 1, end_time: 2, limit: 1000)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_deposit_history
|
|
63
|
+
@client.expects(:get_signed).with("/api/v1/deposit/history", has_entries(coin: "usdt", status: 1, offset: 0, limit: 1000))
|
|
64
|
+
@client.deposit_history(coin: "usdt", status: 1, offset: 0)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_withdraw_history
|
|
68
|
+
@client.expects(:get_signed).with("/api/v1/withdraw/history", has_entries(coin: "usdt", status: 5))
|
|
69
|
+
@client.withdraw_history(coin: "usdt", status: 5)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_withdraw_commits
|
|
73
|
+
@client.expects(:post_signed).with("/api/v1/withdraw/commit",
|
|
74
|
+
has_entries(coin: "usdt", amount: "10", addressTo: "0xabc", chainName: "TRX"))
|
|
75
|
+
@client.withdraw(coin: "usdt", address: "0xabc", amount: "10", network: "TRX")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_get_all_coins_information_reads_exchange_info_chains
|
|
79
|
+
stub_connection(:get, { "coins" => [{ "coin" => "usdt", "chainDetail" => [
|
|
80
|
+
{ "chain" => "OMNI", "enableWithdraw" => false, "enableDeposit" => false, "withdrawFee" => "1.28" },
|
|
81
|
+
{ "chain" => "TRX", "enableWithdraw" => true, "enableDeposit" => true, "withdrawFee" => "1.00",
|
|
82
|
+
"minWithdraw" => "2.00", "maxWithdraw" => "2000000.00" }
|
|
83
|
+
] }] })
|
|
84
|
+
|
|
85
|
+
result = @client.get_all_coins_information
|
|
86
|
+
|
|
87
|
+
assert result.success?
|
|
88
|
+
coin = result.data.first
|
|
89
|
+
assert_equal "USDT", coin["coin"]
|
|
90
|
+
assert_equal [{ "network" => "TRX", "withdrawFee" => "1.00", "withdrawMin" => "2.00", "withdrawMax" => "2000000.00",
|
|
91
|
+
"withdrawEnable" => true, "depositEnable" => true, "isDefault" => false }], coin["networkList"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_ticker_24hr
|
|
95
|
+
@client.expects(:get_public).with("/api/v1/ticker/24hr", { symbol: nil })
|
|
96
|
+
@client.ticker_24hr
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_candlestick_data_sends_from_idx
|
|
100
|
+
@client.expects(:get_public).with("/api/v1/market/kline", has_entries(symbol: "BTCUSDT", scale: "1D", fromIdx: 5, limit: 1440))
|
|
101
|
+
@client.candlestick_data(symbol: "BTCUSDT", interval: "1D", from_idx: 5, limit: 1440)
|
|
102
|
+
end
|
|
103
|
+
|
|
56
104
|
def test_headers_include_api_key
|
|
57
105
|
headers = @client.send(:auth_headers)
|
|
58
106
|
assert_equal "test_key", headers[:"X-MBX-APIKEY"]
|
|
@@ -95,6 +95,21 @@ class Honeymaker::Clients::BitvavoTest < Minitest::Test
|
|
|
95
95
|
assert result.success?
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
def test_deposit_history_path
|
|
99
|
+
@client.expects(:get_signed).with("/v2/depositHistory", { symbol: "BTC", limit: 1000, start: 1, end: 2 })
|
|
100
|
+
@client.get_deposit_history(symbol: "BTC", limit: 1000, start_time: 1, end_time: 2)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_withdrawal_history_path
|
|
104
|
+
@client.expects(:get_signed).with("/v2/withdrawalHistory", { symbol: nil, limit: 1000, start: 1, end: 2 })
|
|
105
|
+
@client.get_withdrawal_history(limit: 1000, start_time: 1, end_time: 2)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_transactions_read_account_history
|
|
109
|
+
@client.expects(:get_signed).with("/v2/account/history", { fromDate: 1, toDate: 2, page: 3, maxItems: 100, type: "deposit" })
|
|
110
|
+
@client.get_transactions(limit: 100, start_time: 1, end_time: 2, page: 3, type: "deposit")
|
|
111
|
+
end
|
|
112
|
+
|
|
98
113
|
def test_get_deposit_history
|
|
99
114
|
stub_connection(:get, [{ "symbol" => "BTC", "amount" => "1.0" }])
|
|
100
115
|
result = @client.get_deposit_history
|
|
@@ -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")
|
|
@@ -89,6 +89,21 @@ class Honeymaker::Clients::GeminiTest < Minitest::Test
|
|
|
89
89
|
assert_equal OpenSSL::HMAC.hexdigest("sha384", "test_secret", payload), request.headers[:"X-GEMINI-SIGNATURE"]
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def test_get_transfers_v2
|
|
93
|
+
@client.expects(:post_signed).with("/v2/transfers", has_entries(timestamp: 1, limit_transfers: 50))
|
|
94
|
+
.returns(Honeymaker::Result::Success.new([{ "eid" => 1, "feeCurrency" => "BTC" }]))
|
|
95
|
+
assert @client.get_transfers_v2(timestamp: 1, limit_transfers: 50).success?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_get_transfers_v2_signs_the_v2_request
|
|
99
|
+
connection = stub
|
|
100
|
+
connection.expects(:post).yields(req = stub_everything).returns(stub(body: []))
|
|
101
|
+
req.expects(:url).with("/v2/transfers")
|
|
102
|
+
req.expects(:headers=).with { |h| JSON.parse(Base64.decode64(h[:"X-GEMINI-PAYLOAD"]))["request"] == "/v2/transfers" }
|
|
103
|
+
@client.instance_variable_set(:@connection, connection)
|
|
104
|
+
assert @client.get_transfers_v2(limit_transfers: 50).success?
|
|
105
|
+
end
|
|
106
|
+
|
|
92
107
|
private
|
|
93
108
|
|
|
94
109
|
def stub_connection(method, body)
|
|
@@ -70,8 +70,10 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
|
|
|
70
70
|
# quote_amount_exec 0 (the job subtracts the quote delta from missed_quote_amount).
|
|
71
71
|
order_body = order_status_body(status: "filled", sz: "0.0", orig_sz: "0.00018", limit_px: "64689.0")
|
|
72
72
|
stub_connection(:post, order_body)
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
# no match; the walk reads on until a page brings nothing new
|
|
74
|
+
@client.expects(:user_fills_by_time).twice
|
|
75
|
+
.returns(Honeymaker::Result::Success.new([fill(oid: 111, px: "1.0", sz: "5.0", tid: 1)]),
|
|
76
|
+
Honeymaker::Result::Success.new([fill(oid: 111, px: "1.0", sz: "5.0", tid: 1)]))
|
|
75
77
|
|
|
76
78
|
result = @client.order_status(user: "0xabc", oid: 123456789)
|
|
77
79
|
|
|
@@ -190,6 +192,80 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
|
|
|
190
192
|
assert result.success?
|
|
191
193
|
end
|
|
192
194
|
|
|
195
|
+
# One userFillsByTime answer is capped; an order whose fills spill past it must still be costed
|
|
196
|
+
# exactly, not estimated from the limit price.
|
|
197
|
+
def test_order_status_reads_fills_past_the_first_page
|
|
198
|
+
order_body = order_status_body(status: "filled", sz: "0.0", orig_sz: "0.00020")
|
|
199
|
+
stub_connection(:post, order_body)
|
|
200
|
+
first = [fill(oid: 1, px: "1.0", sz: "1.0", time: 1781698875556),
|
|
201
|
+
fill(oid: 123456789, px: "64000.0", sz: "0.00010", time: 1781698875600, tid: 7)]
|
|
202
|
+
second = [fill(oid: 123456789, px: "64000.0", sz: "0.00010", time: 1781698875600, tid: 7), # boundary re-read
|
|
203
|
+
fill(oid: 123456789, px: "66000.0", sz: "0.00010", time: 1781698875700)]
|
|
204
|
+
seq = sequence("pages")
|
|
205
|
+
@client.expects(:user_fills_by_time).with(user: "0xabc", start_time: 1781698875556).in_sequence(seq)
|
|
206
|
+
.returns(Honeymaker::Result::Success.new(first))
|
|
207
|
+
@client.expects(:user_fills_by_time).with(user: "0xabc", start_time: 1781698875600).in_sequence(seq)
|
|
208
|
+
.returns(Honeymaker::Result::Success.new(second))
|
|
209
|
+
|
|
210
|
+
result = @client.order_status(user: "0xabc", oid: 123456789)
|
|
211
|
+
|
|
212
|
+
assert_equal BigDecimal("13.0"), result.data[:quote_amount_exec]
|
|
213
|
+
assert_equal BigDecimal("65000"), result.data[:price]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_order_status_propagates_a_failed_later_page
|
|
217
|
+
stub_connection(:post, order_status_body(status: "filled", sz: "0.0", orig_sz: "0.00020"))
|
|
218
|
+
@client.expects(:user_fills_by_time).twice
|
|
219
|
+
.returns(Honeymaker::Result::Success.new([fill(oid: 123456789, px: "64000.0", sz: "0.00010")]),
|
|
220
|
+
Honeymaker::Result::Failure.new("Net::ReadTimeout"))
|
|
221
|
+
|
|
222
|
+
result = @client.order_status(user: "0xabc", oid: 123456789)
|
|
223
|
+
|
|
224
|
+
assert result.failure?
|
|
225
|
+
assert_includes result.errors, "Net::ReadTimeout"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_user_role
|
|
229
|
+
connection = stub
|
|
230
|
+
connection.expects(:post).yields(req = stub_everything).returns(stub(body: { "role" => "agent", "data" => { "user" => "0xm" } }))
|
|
231
|
+
req.expects(:body=).with({ type: "userRole", user: "0xabc" }.to_json)
|
|
232
|
+
@client.instance_variable_set(:@connection, connection)
|
|
233
|
+
|
|
234
|
+
result = @client.user_role(user: "0xabc")
|
|
235
|
+
|
|
236
|
+
assert_equal "agent", result.data["role"]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# == Trading client construction ==
|
|
240
|
+
|
|
241
|
+
def test_trading_client_opens_no_websocket
|
|
242
|
+
with_fake_hyperliquid do
|
|
243
|
+
client = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01")
|
|
244
|
+
assert client.send(:build_exchange).info.skip_ws
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def test_trading_client_sends_through_the_configured_proxy
|
|
249
|
+
with_fake_hyperliquid do
|
|
250
|
+
client = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01", proxy: "http://proxy.local:8080")
|
|
251
|
+
exchange = client.send(:build_exchange)
|
|
252
|
+
[exchange.instance_variable_get(:@transport), exchange.info.transport].each do |transport|
|
|
253
|
+
assert_equal "proxy.local", transport.instance_variable_get(:@connection).proxy.uri.host
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_trading_clients_sign_strictly_increasing_nonces_within_one_millisecond
|
|
259
|
+
with_fake_hyperliquid do
|
|
260
|
+
Time.stubs(:now).returns(Time.at(1_781_698_875.556))
|
|
261
|
+
a = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01").send(:build_exchange)
|
|
262
|
+
b = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01").send(:build_exchange)
|
|
263
|
+
nonces = [a.nonce, b.nonce, a.nonce]
|
|
264
|
+
assert_equal nonces.sort.uniq, nonces
|
|
265
|
+
assert_operator nonces.first, :>=, 1_781_698_875_556
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
193
269
|
# == Raised trading errors keep their HTTP status ==
|
|
194
270
|
#
|
|
195
271
|
# Order placement goes through hyperliquid-rb, which raises instead of answering. Whether the
|
|
@@ -285,8 +361,48 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
|
|
|
285
361
|
}
|
|
286
362
|
end
|
|
287
363
|
|
|
288
|
-
def fill(oid:, px:, sz:, coin: "@142", side: "B")
|
|
364
|
+
def fill(oid:, px:, sz:, coin: "@142", side: "B", time: 1781698875556, tid: nil)
|
|
365
|
+
@tid = (@tid || 0) + 1
|
|
289
366
|
{ "coin" => coin, "oid" => oid, "px" => px, "sz" => sz, "side" => side,
|
|
290
|
-
"time" =>
|
|
367
|
+
"time" => time, "fee" => "0.011", "tid" => tid || @tid }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# hyperliquid-rb's shape, as far as build_exchange touches it. The gem itself is an optional
|
|
371
|
+
# dependency (native secp256k1) that this bundle does not carry.
|
|
372
|
+
module FakeHyperliquid
|
|
373
|
+
class Transport
|
|
374
|
+
def initialize = @connection = Faraday.new(url: "https://api.hyperliquid.xyz")
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
class Info
|
|
378
|
+
attr_reader :transport, :skip_ws
|
|
379
|
+
|
|
380
|
+
def initialize(skip_ws:)
|
|
381
|
+
@skip_ws = skip_ws
|
|
382
|
+
@transport = Transport.new
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
class Exchange
|
|
387
|
+
attr_reader :info
|
|
388
|
+
|
|
389
|
+
def initialize(private_key:, skip_ws: false)
|
|
390
|
+
@info = Info.new(skip_ws: skip_ws)
|
|
391
|
+
@transport = Transport.new
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def nonce = timestamp_ms
|
|
395
|
+
|
|
396
|
+
private
|
|
397
|
+
|
|
398
|
+
def timestamp_ms = (Time.now.to_f * 1000).to_i
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def with_fake_hyperliquid
|
|
403
|
+
Object.const_set(:Hyperliquid, FakeHyperliquid)
|
|
404
|
+
yield
|
|
405
|
+
ensure
|
|
406
|
+
Object.send(:remove_const, :Hyperliquid)
|
|
291
407
|
end
|
|
292
408
|
end
|
|
@@ -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)
|
|
@@ -70,6 +70,18 @@ class Honeymaker::Clients::MexcTest < Minitest::Test
|
|
|
70
70
|
assert result.success?
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def test_capital_withdraw_posts_the_new_endpoint
|
|
74
|
+
@client.expects(:post_signed).with("/api/v3/capital/withdraw",
|
|
75
|
+
has_entries(coin: "USDT", address: "addr", amount: "10", netWork: "TRX"))
|
|
76
|
+
.returns(Honeymaker::Result::Success.new({ "id" => "w1" }))
|
|
77
|
+
assert_equal "w1", @client.capital_withdraw(coin: "USDT", address: "addr", amount: "10", net_work: "TRX").data["id"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_get_all_coins_information_is_signed
|
|
81
|
+
@client.expects(:get_signed).with("/api/v3/capital/config/getall", { recvWindow: 5000 })
|
|
82
|
+
@client.get_all_coins_information
|
|
83
|
+
end
|
|
84
|
+
|
|
73
85
|
def test_withdraw_history
|
|
74
86
|
stub_connection(:get, [{ "coin" => "ETH" }])
|
|
75
87
|
result = @client.withdraw_history
|