/
ikratkiy
/
Hyperliquid
Обзор
Документация
Войти
/
ikratkiy
/
Hyperliquid
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/shared/hyperliquid/unit.test.ts
483 строки
17 KB
ikratkiy
fix(unit): bind recovery to verified deposit identity
14 июл 2026, 21:52
14 июл 2026, 21:52
f680599
Код
Авторство
О чём код?
import { describe, expect, test } from "vitest"; import { buildUnitDepositAddressUrl, buildUnitFeesUrl, buildUnitOperationsUrl, classifyUnitDepositAddress, createUnitClient, extractBitcoinTxId, extractUnitBitcoinDepositFees, findBitcoinDepositOperation, formatUnitOperationState, isUnitOperationDone, parseBitcoinSourceReference, summarizeUnitOperations, verifyUnitGuardianSignatures, } from "./unit"; const beneficiaryAddress = "0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D"; const mainnetUnitAddress = "bc1phlteskaaznw3l65vtn0xkrne7y46xgzqxrqvrued5t3eycjmca8qefvldf"; const testnetUnitAddress = "tb1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsn60vlk"; const txidA = "a".repeat(64); const txidB = "b".repeat(64); /** * Public Mainnet fixture read from Unit on 2026-07-14T21:28:19+03:00: * GET /gen/bitcoin/hyperliquid/btc/0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D */ const mainnetGuardianFixture = { address: mainnetUnitAddress, signatures: { "field-node": "dHJASa7zzfTO6iv/SouBinXqLbQbIsyorvAUN4f57iTK/gSMElDJRrrXOQbCQT6Ae9C4nATJto/gfekIJ9kf8Q==", "hl-node": "pqlj5MyyFxaN3khruO6lhDgBKJP7Hmn6b/9sdcR7aKsfwRCfZTMAIy7E5EZx/umJaqH3UhroJbIcI7U5whl1og==", "unit-node": "F5tsNl/3xhZHPY5wICnLe07ADcNHnM4RQIlIQYAqYQOnnoDi3sB0s9PPuOdWUhJSitjiV+GU4fq9izQa8fJJ3g==", }, status: "OK", }; const testnetUnitResponse = { address: testnetUnitAddress, signatures: { "field-node": "sig-a", "hl-node-testnet": "sig-b", "node-1": "sig-c", }, status: "OK", }; const depositRoute = { asset: "btc", protocolAddress: mainnetUnitAddress, destinationAddress: beneficiaryAddress, }; function bitcoinDepositOperation(overrides: Record<string, unknown> = {}) { return { operationId: "unit-op-1", opCreatedAt: "2026-07-14T08:00:00.000Z", sourceChain: "bitcoin", destinationChain: "hyperliquid", asset: "btc", state: "done", sourceTxHash: `${txidA}:0`, protocolAddress: mainnetUnitAddress, sourceAddress: "bc1qexample-source", destinationAddress: beneficiaryAddress, sourceAmount: "0.00210000", destinationFeeAmount: "0.00002840", sweepFeeAmount: "0.00000100", destinationTxHash: "0x1234", sourceTxConfirmations: 6, destinationTxConfirmations: 1, ...overrides, }; } describe("Unit client helpers", () => { test("builds deposit, operations, and fee URLs", () => { expect(buildUnitDepositAddressUrl("https://api.hyperunit.xyz/", beneficiaryAddress)).toBe( `https://api.hyperunit.xyz/gen/bitcoin/hyperliquid/btc/${beneficiaryAddress.toLowerCase()}`, ); expect(buildUnitOperationsUrl("https://api.hyperunit.xyz/", beneficiaryAddress)).toBe( `https://api.hyperunit.xyz/operations/${beneficiaryAddress.toLowerCase()}`, ); expect(buildUnitFeesUrl("https://api.hyperunit.xyz/")).toBe("https://api.hyperunit.xyz/v2/estimate-fees"); }); test("creates a fetch-backed Unit client", async () => { const fetcher = async () => ({ ok: true, status: 200, statusText: "OK", text: async () => JSON.stringify(testnetUnitResponse), }) as const; const client = createUnitClient("https://api.hyperunit-testnet.xyz", fetcher); await expect(client.getBtcDepositAddress(beneficiaryAddress)).resolves.toEqual(testnetUnitResponse); }); }); describe("Unit deposit address validation", () => { test("classifies valid testnet-family and Mainnet addresses", () => { expect(classifyUnitDepositAddress(testnetUnitResponse, { bitcoinNetwork: "signet" })).toMatchObject({ status: "ok", address: testnetUnitAddress, signatureCount: 3, }); expect(classifyUnitDepositAddress(mainnetGuardianFixture, { bitcoinNetwork: "mainnet" })).toMatchObject({ status: "ok", address: mainnetUnitAddress, signatureCount: 3, }); }); test("rejects the wrong Bitcoin network and invalid checksums", () => { expect(classifyUnitDepositAddress(testnetUnitResponse, { bitcoinNetwork: "mainnet" })).toMatchObject({ status: "error", reason: "Unit returned a non-mainnet Bitcoin address.", }); expect( classifyUnitDepositAddress( { ...mainnetGuardianFixture, address: "bc1punitdepositaddress" }, { bitcoinNetwork: "mainnet" }, ), ).toMatchObject({ status: "error", reason: "Unit returned an invalid Bitcoin mainnet address or checksum.", }); }); }); describe("Unit Mainnet guardian signatures", () => { test("verifies the public Mainnet fixture as 3-of-3 using real WebCrypto", async () => { await expect( verifyUnitGuardianSignatures(mainnetGuardianFixture, { bitcoinNetwork: "mainnet", destinationAddress: beneficiaryAddress, }), ).resolves.toEqual({ status: "verified", verificationMethod: "unit-guardian-signatures", signatureCount: 3, verifiedCount: 3, raw: mainnetGuardianFixture, }); }); test("accepts exactly 2 valid Mainnet signatures", async () => { const twoOfThreeFixture = { ...mainnetGuardianFixture, signatures: { "field-node": mainnetGuardianFixture.signatures["field-node"], "unit-node": mainnetGuardianFixture.signatures["unit-node"], }, }; await expect( verifyUnitGuardianSignatures(twoOfThreeFixture, { bitcoinNetwork: "mainnet", destinationAddress: beneficiaryAddress, }), ).resolves.toMatchObject({ status: "verified", signatureCount: 2, verifiedCount: 2, }); }); test("rejects the fixture when the signed beneficiary changes", async () => { await expect( verifyUnitGuardianSignatures(mainnetGuardianFixture, { bitcoinNetwork: "mainnet", destinationAddress: "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1e", }), ).resolves.toMatchObject({ status: "failed", verifiedCount: 0, }); }); test("rejects the fixture when the signed Bitcoin address changes", async () => { await expect( verifyUnitGuardianSignatures( { ...mainnetGuardianFixture, address: "bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e" }, { bitcoinNetwork: "mainnet", destinationAddress: beneficiaryAddress }, ), ).resolves.toMatchObject({ status: "failed", verifiedCount: 0, }); }); test("rejects a fixture with fewer than 2 valid signatures without mocking WebCrypto", async () => { const zeroSignature = globalThis.btoa(String.fromCharCode(...new Uint8Array(64))); const corruptedFixture = { ...mainnetGuardianFixture, signatures: { "field-node": zeroSignature, "hl-node": zeroSignature, "unit-node": mainnetGuardianFixture.signatures["unit-node"], }, }; await expect( verifyUnitGuardianSignatures(corruptedFixture, { bitcoinNetwork: "mainnet", destinationAddress: beneficiaryAddress, }), ).resolves.toMatchObject({ status: "failed", signatureCount: 3, verifiedCount: 1, }); }); test("requires the beneficiary and rejects unknown guardians", async () => { await expect(verifyUnitGuardianSignatures(mainnetGuardianFixture, { bitcoinNetwork: "mainnet" })).resolves.toMatchObject({ status: "failed", reason: "Для проверки подписей Unit нужен EVM-адрес получателя Hyperliquid.", }); const raw = { ...mainnetGuardianFixture, signatures: { ...mainnetGuardianFixture.signatures, "unexpected-node": mainnetGuardianFixture.signatures["field-node"], }, }; await expect( verifyUnitGuardianSignatures(raw, { bitcoinNetwork: "mainnet", destinationAddress: beneficiaryAddress, }), ).resolves.toMatchObject({ status: "failed", reason: "Unit вернул подписи неизвестных guardians для выбранной сети: unexpected-node.", verifiedCount: 0, }); }); }); describe("Bitcoin source references", () => { test("parses bare txid and canonicalizes txid:vout", () => { const uppercaseTxid = "A".repeat(64); expect(parseBitcoinSourceReference(uppercaseTxid)).toEqual({ reference: txidA, txid: txidA, vout: null, }); expect(parseBitcoinSourceReference(`${uppercaseTxid}:0002`)).toEqual({ reference: `${txidA}:2`, txid: txidA, vout: 2, }); expect(extractBitcoinTxId(`${uppercaseTxid}:2`)).toBe(txidA); }); test.each(["abc", `0x${txidA}`, `${txidA}:`, `${txidA}:-1`, `${txidA}:1.5`, `${txidA}:4294967296`])( "rejects invalid Bitcoin source reference %s", (reference) => { expect(parseBitcoinSourceReference(reference)).toBeNull(); expect(extractBitcoinTxId(reference)).toBe(""); }, ); test("accepts the uint32 vout boundaries", () => { expect(parseBitcoinSourceReference(`${txidA}:0`)).toMatchObject({ vout: 0 }); expect(parseBitcoinSourceReference(`${txidA}:4294967295`)).toEqual({ reference: `${txidA}:4294967295`, txid: txidA, vout: 4_294_967_295, }); }); }); describe("Unit Bitcoin operation identity", () => { test("summarizes only structurally valid Bitcoin deposit operations", () => { expect( summarizeUnitOperations({ operations: [bitcoinDepositOperation(), bitcoinDepositOperation({ operationId: "missing-time", opCreatedAt: undefined })], }), ).toEqual([ { operationId: "unit-op-1", opCreatedAt: "2026-07-14T08:00:00.000Z", asset: "btc", state: "done", sourceTxHash: `${txidA}:0`, }, ]); }); test("matches the exact asset, Unit protocol address, beneficiary, txid, and vout", () => { const exact = bitcoinDepositOperation({ operationId: "exact", sourceTxHash: `${txidA}:2` }); const lookup = findBitcoinDepositOperation( { operations: [ bitcoinDepositOperation({ operationId: "wrong-asset", asset: "eth", sourceTxHash: `${txidA}:2` }), bitcoinDepositOperation({ operationId: "wrong-protocol", protocolAddress: "bc1qother", sourceTxHash: `${txidA}:2` }), bitcoinDepositOperation({ operationId: "wrong-beneficiary", destinationAddress: "0x1111111111111111111111111111111111111111", sourceTxHash: `${txidA}:2`, }), exact, ], }, { ...depositRoute, sourceTxHash: `${txidA}:2` }, ); expect(lookup).toMatchObject({ status: "found", operation: { operationId: "exact", sourceTxId: txidA, sourceTxVout: 2, sourceTxReference: `${txidA}:2`, protocolAddress: mainnetUnitAddress, destinationAddress: beneficiaryAddress, }, }); if (lookup.status !== "found") { throw new Error("Expected the exact Unit operation to be found."); } expect(isUnitOperationDone(lookup.operation)).toBe(true); expect(formatUnitOperationState(lookup.operation)).toBe("Готово: Unit зачислил BTC на Hyperliquid."); }); test.each([ ["another asset", { asset: "eth" }], ["another protocol address", { protocolAddress: "bc1qother" }], ["another beneficiary", { destinationAddress: "0x1111111111111111111111111111111111111111" }], ["missing protocol address", { protocolAddress: "" }], ["missing beneficiary", { destinationAddress: "" }], ])("does not accept an operation with %s", (_label, overrides) => { expect( findBitcoinDepositOperation( { operations: [bitcoinDepositOperation(overrides)] }, { ...depositRoute, sourceTxHash: txidA }, ), ).toMatchObject({ status: "not-found" }); }); test("requires txid:vout when more than one output matches a wallet txid", () => { const raw = { operations: [ bitcoinDepositOperation({ operationId: "vout-0", sourceTxHash: `${txidA}:0` }), bitcoinDepositOperation({ operationId: "vout-1", sourceTxHash: `${txidA}:1` }), ], }; expect(findBitcoinDepositOperation(raw, { ...depositRoute, sourceTxHash: txidA })).toMatchObject({ status: "ambiguous", matches: [{ sourceTxVout: 0 }, { sourceTxVout: 1 }], }); expect(findBitcoinDepositOperation(raw, { ...depositRoute, sourceTxHash: `${txidA}:1` })).toMatchObject({ status: "found", operation: { operationId: "vout-1", sourceTxVout: 1 }, }); }); test.each([ ["missing timestamp", { opCreatedAt: undefined }], ["malformed timestamp", { opCreatedAt: "not-a-date" }], ["date-only timestamp", { opCreatedAt: "2026-07-14" }], ["permissive Date.parse input", { opCreatedAt: "0" }], ["impossible calendar date", { opCreatedAt: "2026-02-30T10:00:00Z" }], ["bare source txid", { sourceTxHash: txidA }], ["malformed source reference", { sourceTxHash: "source-tx" }], ])("fails closed for a route operation with %s", (_label, overrides) => { expect( findBitcoinDepositOperation( { operations: [bitcoinDepositOperation(overrides)] }, { ...depositRoute, sourceTxHash: txidA }, ), ).toMatchObject({ status: "malformed-response" }); }); test("fails closed when any exact-route record is malformed instead of selecting another record", () => { expect( findBitcoinDepositOperation( { operations: [ bitcoinDepositOperation({ operationId: "valid", sourceTxHash: `${txidA}:0` }), bitcoinDepositOperation({ operationId: "invalid-time", sourceTxHash: `${txidB}:0`, opCreatedAt: "invalid" }), ], }, { ...depositRoute, sourceTxHash: txidA }, ), ).toMatchObject({ status: "malformed-response" }); }); test("rejects an invalid requested txid and malformed operations envelopes", () => { expect( findBitcoinDepositOperation( { operations: [bitcoinDepositOperation()] }, { ...depositRoute, sourceTxHash: "missing-tx" }, ), ).toMatchObject({ status: "malformed-response" }); expect( findBitcoinDepositOperation( { operationz: [] }, { ...depositRoute, sourceTxHash: txidA }, ), ).toEqual({ status: "malformed-response", reason: "Ответ Unit operations не содержит массив operations.", raw: { operationz: [] }, }); }); test("reports an in-progress matched operation", () => { const lookup = findBitcoinDepositOperation( { operations: [bitcoinDepositOperation({ state: "waitForSrcTxFinalization" })] }, { ...depositRoute, sourceTxHash: txidA }, ); expect(lookup.status).toBe("found"); if (lookup.status !== "found") { throw new Error("Expected Unit operation to be found."); } expect(isUnitOperationDone(lookup.operation)).toBe(false); expect(formatUnitOperationState(lookup.operation)).toBe("Ожидаем подтверждения BTC-транзакции в сети Bitcoin."); }); test.each(["DONE", "Done", "completed", "complete", "success", "succeeded", "finished"])( "does not treat undocumented terminal alias %s as a completed deposit", (state) => { expect(isUnitOperationDone({ asset: "btc", state })).toBe(false); expect(formatUnitOperationState({ asset: "btc", state })).not.toContain("Готово:"); }, ); test("explains when Unit has discovered the source Bitcoin transaction", () => { expect(formatUnitOperationState({ asset: "btc", state: "sourceTxDiscovered" })).toBe( "Unit увидел BTC-транзакцию и ждёт финализацию перед зачислением в Hyperliquid.", ); expect(formatUnitOperationState({ asset: "btc", state: "srcTxDiscovered" })).toBe( "Unit увидел BTC-транзакцию и ждёт финализацию перед зачислением в Hyperliquid.", ); }); }); describe("Unit Bitcoin fee estimate", () => { test("extracts numeric and numeric-string fee estimates", () => { expect( extractUnitBitcoinDepositFees({ bitcoin: { "deposit-fee-in-units": 0.0000284040522507, "deposit-fee-rate-sats-per-vb": 8, "deposit-size-v-bytes": 113, depositEta: "31m", depositFee: 2840, }, }), ).toEqual({ depositEta: "31m", depositFeeBtc: "0.0000284", depositFeeRateSatsPerVb: 8, depositFeeSats: 2840, depositSizeVBytes: 113, }); expect( extractUnitBitcoinDepositFees({ bitcoin: { "deposit-fee-rate-sats-per-vb": "12", "deposit-size-v-bytes": "141", depositEta: "18m", depositFee: "1692", }, }), ).toEqual({ depositEta: "18m", depositFeeBtc: "0.00001692", depositFeeRateSatsPerVb: 12, depositFeeSats: 1692, depositSizeVBytes: 141, }); }); });