hodl-wallet 2.0.4 → 2.0.6
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.
- package/README.md +79 -2
- package/dist/agent-cli.js +75 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-errors.d.ts +1 -1
- package/dist/agent-errors.js.map +1 -1
- package/dist/index.js +148 -82
- package/dist/index.js.map +1 -1
- package/dist/network/arb.d.ts +1 -0
- package/dist/network/arb.js +2 -1
- package/dist/network/arb.js.map +1 -1
- package/dist/network/avax.d.ts +1 -0
- package/dist/network/avax.js +2 -1
- package/dist/network/avax.js.map +1 -1
- package/dist/network/bsc.d.ts +1 -0
- package/dist/network/bsc.js +2 -1
- package/dist/network/bsc.js.map +1 -1
- package/dist/network/eth.d.ts +1 -0
- package/dist/network/eth.js +2 -1
- package/dist/network/eth.js.map +1 -1
- package/dist/network/ftm.d.ts +1 -0
- package/dist/network/ftm.js +2 -1
- package/dist/network/ftm.js.map +1 -1
- package/dist/network/hyperliquid.d.ts +1 -0
- package/dist/network/hyperliquid.js +2 -1
- package/dist/network/hyperliquid.js.map +1 -1
- package/dist/network/lib/BitcoinNetwork.d.ts +2 -0
- package/dist/network/lib/BitcoinNetwork.js +23 -15
- package/dist/network/lib/BitcoinNetwork.js.map +1 -1
- package/dist/network/lib/Web3Network.js +14 -2
- package/dist/network/lib/Web3Network.js.map +1 -1
- package/dist/network/op.d.ts +1 -0
- package/dist/network/op.js +2 -1
- package/dist/network/op.js.map +1 -1
- package/dist/network/pol.d.ts +1 -0
- package/dist/network/pol.js +2 -1
- package/dist/network/pol.js.map +1 -1
- package/dist/network/types.d.ts +1 -0
- package/dist/network-registry.d.ts +1 -0
- package/dist/network-registry.js +3 -0
- package/dist/network-registry.js.map +1 -1
- package/dist/persist.d.ts +2 -0
- package/dist/persist.js +21 -0
- package/dist/persist.js.map +1 -1
- package/dist/profile-lock.d.ts +4 -0
- package/dist/profile-lock.js +9 -1
- package/dist/profile-lock.js.map +1 -1
- package/dist/swap/bitcoin.d.ts +34 -0
- package/dist/swap/bitcoin.js +179 -0
- package/dist/swap/bitcoin.js.map +1 -0
- package/dist/swap/chain.d.ts +49 -0
- package/dist/swap/chain.js +255 -0
- package/dist/swap/chain.js.map +1 -0
- package/dist/swap/config.d.ts +68 -0
- package/dist/swap/config.js +46 -0
- package/dist/swap/config.js.map +1 -0
- package/dist/swap/http.d.ts +14 -0
- package/dist/swap/http.js +70 -0
- package/dist/swap/http.js.map +1 -0
- package/dist/swap/providers.d.ts +24 -0
- package/dist/swap/providers.js +424 -0
- package/dist/swap/providers.js.map +1 -0
- package/dist/swap/routes.d.ts +145 -0
- package/dist/swap/routes.js +48 -0
- package/dist/swap/routes.js.map +1 -0
- package/dist/swap/service.d.ts +104 -0
- package/dist/swap/service.js +512 -0
- package/dist/swap/service.js.map +1 -0
- package/dist/swap/storage.d.ts +3 -0
- package/dist/swap/storage.js +28 -0
- package/dist/swap/storage.js.map +1 -0
- package/dist/swap/types.d.ts +140 -0
- package/dist/swap/types.js +2 -0
- package/dist/swap/types.js.map +1 -0
- package/dist/swap/ui.d.ts +3 -0
- package/dist/swap/ui.js +257 -0
- package/dist/swap/ui.js.map +1 -0
- package/dist/transfer-service.d.ts +38 -0
- package/dist/transfer-service.js +181 -0
- package/dist/transfer-service.js.map +1 -0
- package/dist/wallet-service.d.ts +22 -6
- package/dist/wallet-service.js +36 -171
- package/dist/wallet-service.js.map +1 -1
- package/package.json +99 -92
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import bsc from '../network/bsc.js';
|
|
2
|
+
import btc from '../network/btc.js';
|
|
3
|
+
export const swapConfig = {
|
|
4
|
+
source: bsc,
|
|
5
|
+
destination: btc,
|
|
6
|
+
asset: 'USDT',
|
|
7
|
+
token: bsc.tokens.USDT.address,
|
|
8
|
+
decimals: 18,
|
|
9
|
+
slippageBps: 50,
|
|
10
|
+
bitcoinConfirmations: 3,
|
|
11
|
+
bscConfirmations: 15,
|
|
12
|
+
httpTimeoutMs: 15_000,
|
|
13
|
+
pollIntervalMs: 10_000,
|
|
14
|
+
quoteLifetimeMs: 300_000,
|
|
15
|
+
referenceMaxAgeMs: 300_000,
|
|
16
|
+
clockSkewMs: 60_000,
|
|
17
|
+
fundingMarginMs: 30_000,
|
|
18
|
+
attentionAfterMs: 3_600_000,
|
|
19
|
+
bitcoin: {
|
|
20
|
+
changeDustSats: 546,
|
|
21
|
+
memoDustSats: 294,
|
|
22
|
+
maxOutputs: 10,
|
|
23
|
+
feeTargetBlocks: 1
|
|
24
|
+
},
|
|
25
|
+
gas: {
|
|
26
|
+
approval: 100_000,
|
|
27
|
+
transfer: 120_000,
|
|
28
|
+
deposit: 350_000
|
|
29
|
+
},
|
|
30
|
+
chainflip: {
|
|
31
|
+
url: 'https://chainflip-swap.chainflip.io',
|
|
32
|
+
blockSeconds: 6,
|
|
33
|
+
refundRetryBlocks: 100,
|
|
34
|
+
sourceAsset: 'BscUsdt'
|
|
35
|
+
},
|
|
36
|
+
thorchain: {
|
|
37
|
+
midgardUrl: 'https://gateway.liquify.com/chain/thorchain_midgard',
|
|
38
|
+
streamingInterval: 1,
|
|
39
|
+
streamingQuantity: 0,
|
|
40
|
+
url: 'https://gateway.liquify.com/chain/thorchain_api',
|
|
41
|
+
router: '0xb30ec53f98ff5947ede720d32ac2da7e52a5f56b',
|
|
42
|
+
asset: `BSC.USDT-${bsc.tokens.USDT.address.toUpperCase()}`
|
|
43
|
+
},
|
|
44
|
+
pricesUrl: 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,binancecoin,tether&vs_currencies=usd&include_last_updated_at=true&precision=8'
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../swap/config.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,mBAAmB,CAAC;AACpC,OAAO,GAAG,MAAM,mBAAmB,CAAC;AAEpC,MAAM,CAAC,MAAM,UAAU,GAAG;IACtB,MAAM,EAAE,GAAG;IACX,WAAW,EAAE,GAAG;IAChB,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO;IAC9B,QAAQ,EAAE,EAAE;IACZ,WAAW,EAAE,EAAE;IACf,oBAAoB,EAAE,CAAC;IACvB,gBAAgB,EAAE,EAAE;IACpB,aAAa,EAAE,MAAM;IACrB,cAAc,EAAE,MAAM;IACtB,eAAe,EAAE,OAAO;IACxB,iBAAiB,EAAE,OAAO;IAC1B,WAAW,EAAE,MAAM;IACnB,eAAe,EAAE,MAAM;IACvB,gBAAgB,EAAE,SAAS;IAC3B,OAAO,EAAE;QACL,cAAc,EAAE,GAAG;QACnB,YAAY,EAAE,GAAG;QACjB,UAAU,EAAE,EAAE;QACd,eAAe,EAAE,CAAC;KACrB;IACD,GAAG,EAAE;QACD,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,OAAO;KACnB;IACD,SAAS,EAAE;QACP,GAAG,EAAE,qCAAqC;QAC1C,YAAY,EAAE,CAAC;QACf,iBAAiB,EAAE,GAAG;QACtB,WAAW,EAAE,SAAS;KACzB;IACD,SAAS,EAAE;QACP,UAAU,EAAE,qDAAqD;QACjE,iBAAiB,EAAE,CAAC;QACpB,iBAAiB,EAAE,CAAC;QACpB,GAAG,EAAE,iDAAiD;QACtD,MAAM,EAAE,4CAA4C;QACpD,KAAK,EAAE,YAAY,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE;KAC7D;IACD,SAAS,EAAE,yIAAyI;CAC9I,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class SwapHttpError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
constructor(status: number, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function swapText(url: string): Promise<string>;
|
|
6
|
+
export declare function swapJson(url: string, body?: unknown): Promise<unknown>;
|
|
7
|
+
export declare function record(value: unknown): Record<string, unknown>;
|
|
8
|
+
export declare function list(value: unknown): unknown[];
|
|
9
|
+
export declare function textField(value: unknown): string;
|
|
10
|
+
export declare function units(value: unknown): bigint;
|
|
11
|
+
export declare function integer(value: unknown): number;
|
|
12
|
+
export declare function evmAddress(value: unknown): string;
|
|
13
|
+
export declare function transactionHash(value: unknown, evm?: boolean): string;
|
|
14
|
+
export declare function ceilDiv(a: bigint, b: bigint): bigint;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { swapConfig } from './config.js';
|
|
2
|
+
export class SwapHttpError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(status, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function swapText(url) {
|
|
10
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(swapConfig.httpTimeoutMs), redirect: 'error' });
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
throw new SwapHttpError(response.status, `Provider request failed (HTTP ${response.status}).`);
|
|
13
|
+
return response.text();
|
|
14
|
+
}
|
|
15
|
+
export async function swapJson(url, body) {
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
18
|
+
headers: { accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }) },
|
|
19
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
20
|
+
signal: AbortSignal.timeout(swapConfig.httpTimeoutMs),
|
|
21
|
+
redirect: 'error'
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new SwapHttpError(response.status, `Provider request failed (HTTP ${response.status}).`);
|
|
25
|
+
}
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
export function record(value) {
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
throw new Error('Invalid provider response.');
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
export function list(value) {
|
|
34
|
+
if (!Array.isArray(value))
|
|
35
|
+
throw new Error('Invalid provider list.');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function textField(value) {
|
|
39
|
+
if (typeof value !== 'string' || !value)
|
|
40
|
+
throw new Error('Missing provider field.');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
export function units(value) {
|
|
44
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value))
|
|
45
|
+
throw new Error('Invalid provider amount.');
|
|
46
|
+
return BigInt(value);
|
|
47
|
+
}
|
|
48
|
+
export function integer(value) {
|
|
49
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
50
|
+
throw new Error('Invalid provider integer.');
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
export function evmAddress(value) {
|
|
54
|
+
const address = textField(value);
|
|
55
|
+
if (!/^0x[\da-f]{40}$/i.test(address) || /^0x0{40}$/i.test(address))
|
|
56
|
+
throw new Error('Invalid provider EVM address.');
|
|
57
|
+
return address;
|
|
58
|
+
}
|
|
59
|
+
export function transactionHash(value, evm = false) {
|
|
60
|
+
const hash = textField(value).replace(/^0x/i, '');
|
|
61
|
+
if (!/^[\da-f]{64}$/i.test(hash) || /^0{64}$/.test(hash))
|
|
62
|
+
throw new Error('Invalid transaction hash.');
|
|
63
|
+
return `${evm ? '0x' : ''}${hash.toLowerCase()}`;
|
|
64
|
+
}
|
|
65
|
+
export function ceilDiv(a, b) {
|
|
66
|
+
if (a < 0n || b <= 0n)
|
|
67
|
+
throw new Error('Invalid price calculation.');
|
|
68
|
+
return (a + b - 1n) / b;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../swap/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SwapRoute } from './routes.js';
|
|
2
|
+
import type { ProviderProgress, ProviderQuote, SwapInput, SwapOperation, SwapPlan, SwapProvider, SwapQuote } from './types.js';
|
|
3
|
+
export declare class ThorchainSwapProvider implements SwapProvider {
|
|
4
|
+
private readonly route;
|
|
5
|
+
readonly id: "thorchain";
|
|
6
|
+
constructor(route?: SwapRoute);
|
|
7
|
+
private inbound;
|
|
8
|
+
quote(input: SwapInput): Promise<ProviderQuote>;
|
|
9
|
+
prepare(quote: SwapQuote): Promise<SwapPlan>;
|
|
10
|
+
validate(quote: SwapQuote, plan: SwapPlan): Promise<void>;
|
|
11
|
+
status(operation: SwapOperation): Promise<ProviderProgress>;
|
|
12
|
+
}
|
|
13
|
+
export declare class ChainflipSwapProvider implements SwapProvider {
|
|
14
|
+
private readonly route;
|
|
15
|
+
readonly id: "chainflip";
|
|
16
|
+
constructor(route?: SwapRoute);
|
|
17
|
+
private available;
|
|
18
|
+
quote(input: SwapInput): Promise<ProviderQuote>;
|
|
19
|
+
prepare(quote: SwapQuote): Promise<SwapPlan>;
|
|
20
|
+
private checkStatus;
|
|
21
|
+
validate(quote: SwapQuote, plan: SwapPlan): Promise<void>;
|
|
22
|
+
status(operation: SwapOperation): Promise<ProviderProgress>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createSwapProviders(route?: SwapRoute): SwapProvider[];
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { assetAddress, sameAssetAddress, swapRoutes } from './routes.js';
|
|
2
|
+
import { swapConfig as config } from './config.js';
|
|
3
|
+
import { ceilDiv, integer, list, record, swapJson, SwapHttpError, textField, transactionHash, units } from './http.js';
|
|
4
|
+
function minimumOutput(expected) {
|
|
5
|
+
if (expected <= 0n)
|
|
6
|
+
throw new Error('The swap has no payable output.');
|
|
7
|
+
return ceilDiv(expected * BigInt(10_000 - config.slippageBps), 10000n).toString();
|
|
8
|
+
}
|
|
9
|
+
function assertTime(expiresAt) {
|
|
10
|
+
if (expiresAt <= Date.now() + config.fundingMarginMs)
|
|
11
|
+
throw new Error('Quote or deposit channel expired; obtain a new quote.');
|
|
12
|
+
}
|
|
13
|
+
function sameAddress(left, right) {
|
|
14
|
+
return typeof left === 'string' && left.toLowerCase() === right.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function decimalRatio(value) {
|
|
17
|
+
const input = textField(value);
|
|
18
|
+
if (!/^\d+(?:\.\d+)?$/.test(input))
|
|
19
|
+
throw new Error('Invalid provider price.');
|
|
20
|
+
const [whole, fraction = ''] = input.split('.');
|
|
21
|
+
return [BigInt(whole + fraction), 10n ** BigInt(fraction.length)];
|
|
22
|
+
}
|
|
23
|
+
function thorMemo(quote, route) {
|
|
24
|
+
const interval = quote.details.streaming === true ? integer(quote.details.streamingInterval) : 1;
|
|
25
|
+
const quantity = quote.details.streaming === true ? integer(quote.details.streamingQuantity) : 1;
|
|
26
|
+
return `=:${route.destination.thorchain.asset}:${quote.to}/${quote.from}:${BigInt(quote.minimumBaseUnits) / route.destination.thorchain.scale}/${interval}/${quantity}`;
|
|
27
|
+
}
|
|
28
|
+
export class ThorchainSwapProvider {
|
|
29
|
+
route;
|
|
30
|
+
id = 'thorchain';
|
|
31
|
+
constructor(route = swapRoutes['bsc-btc']) {
|
|
32
|
+
this.route = route;
|
|
33
|
+
}
|
|
34
|
+
async inbound() {
|
|
35
|
+
const entries = list(await swapJson(`${config.thorchain.url}/thorchain/inbound_addresses`)).map(record);
|
|
36
|
+
for (const chain of ['BSC', 'BTC']) {
|
|
37
|
+
const entry = entries.find(item => item.chain === chain);
|
|
38
|
+
if (!entry)
|
|
39
|
+
throw new Error(`THORChain ${chain} trading is unavailable: chain not listed by the provider API.`);
|
|
40
|
+
const reasons = [];
|
|
41
|
+
for (const [field, reason] of [
|
|
42
|
+
['halted', 'chain halted'],
|
|
43
|
+
['global_trading_paused', 'global trading paused'],
|
|
44
|
+
['chain_trading_paused', 'chain trading paused']
|
|
45
|
+
]) {
|
|
46
|
+
if (entry[field] === true)
|
|
47
|
+
reasons.push(reason);
|
|
48
|
+
else if (entry[field] !== false)
|
|
49
|
+
reasons.push(`${reason} status unverified`);
|
|
50
|
+
}
|
|
51
|
+
if (reasons.length) {
|
|
52
|
+
throw new Error(`THORChain ${chain} trading is unavailable: ${reasons.join('; ')}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const bsc = entries.find(item => item.chain === 'BSC');
|
|
56
|
+
if (!sameAddress(bsc.router, config.thorchain.router))
|
|
57
|
+
throw new Error('THORChain router changed; integration must be reviewed.');
|
|
58
|
+
const source = entries.find(item => item.chain === this.route.source.thorchain.chain);
|
|
59
|
+
assetAddress(this.route.source, source.address);
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
async quote(input) {
|
|
63
|
+
if (input.routeId !== this.route.id)
|
|
64
|
+
throw new Error('Swap route mismatch.');
|
|
65
|
+
const amount = BigInt(input.amountBaseUnits);
|
|
66
|
+
const { source, destination } = this.route;
|
|
67
|
+
if (amount % source.thorchain.scale !== 0n)
|
|
68
|
+
throw new Error('THORChain supports at most 8 USDT decimal places.');
|
|
69
|
+
const inbound = await this.inbound();
|
|
70
|
+
const params = new URLSearchParams({
|
|
71
|
+
from_asset: source.thorchain.asset, to_asset: destination.thorchain.asset, amount: (amount / source.thorchain.scale).toString(),
|
|
72
|
+
destination: input.to, refund_address: input.from,
|
|
73
|
+
...(source.network.family === 'bitcoin' ? { extended: 'true' } : {}),
|
|
74
|
+
streaming_interval: String(config.thorchain.streamingInterval), streaming_quantity: String(config.thorchain.streamingQuantity), liquidity_tolerance_bps: String(config.slippageBps)
|
|
75
|
+
});
|
|
76
|
+
const data = record(await swapJson(`${config.thorchain.url}/thorchain/quote/swap?${params}`));
|
|
77
|
+
if (data.error || (source.network.family === 'evm' && !sameAddress(data.router, config.thorchain.router)) || !sameAssetAddress(source, data.inbound_address, textField(inbound.address))) {
|
|
78
|
+
throw new Error('THORChain returned an unavailable or inconsistent route.');
|
|
79
|
+
}
|
|
80
|
+
const recommendedMinimum = units(data.recommended_min_amount_in);
|
|
81
|
+
if (amount / source.thorchain.scale < recommendedMinimum)
|
|
82
|
+
throw new Error('Amount is below the safe THORChain minimum, including refund fees.');
|
|
83
|
+
const fees = record(data.fees);
|
|
84
|
+
if (textField(fees.asset).toUpperCase() !== destination.thorchain.asset || units(fees.affiliate) !== 0n)
|
|
85
|
+
throw new Error('Unexpected THORChain fees.');
|
|
86
|
+
const expectedThorUnits = units(data.expected_amount_out);
|
|
87
|
+
const expected = expectedThorUnits * destination.thorchain.scale;
|
|
88
|
+
const minimumBaseUnits = (BigInt(minimumOutput(expectedThorUnits)) * destination.thorchain.scale).toString();
|
|
89
|
+
const expiresAt = Math.min(integer(data.expiry) * 1_000, Date.now() + config.quoteLifetimeMs);
|
|
90
|
+
assertTime(expiresAt);
|
|
91
|
+
const evm = source.network.family === 'evm';
|
|
92
|
+
if (data.gas_rate_units !== (evm ? 'gwei' : 'satsperbyte'))
|
|
93
|
+
throw new Error('Unexpected THORChain gas units.');
|
|
94
|
+
if (!evm && amount <= units(inbound.dust_threshold))
|
|
95
|
+
throw new Error('Bitcoin deposit is below the THORChain dust threshold.');
|
|
96
|
+
const gasPrice = units(data.recommended_gas_rate) * (evm ? 1000000000n : 1n);
|
|
97
|
+
return {
|
|
98
|
+
provider: this.id, expectedBaseUnits: expected.toString(), minimumBaseUnits, expiresAt,
|
|
99
|
+
estimatedSeconds: typeof data.total_swap_seconds === 'number' ? integer(data.total_swap_seconds) : null,
|
|
100
|
+
fees: [
|
|
101
|
+
{ label: 'Liquidity / price impact', asset: destination.asset, amountBaseUnits: (units(fees.liquidity) * destination.thorchain.scale).toString(), decimals: destination.decimals },
|
|
102
|
+
{ label: `${destination.asset} payout`, asset: destination.asset, amountBaseUnits: (units(fees.outbound) * destination.thorchain.scale).toString(), decimals: destination.decimals }
|
|
103
|
+
],
|
|
104
|
+
details: {
|
|
105
|
+
depositAddress: assetAddress(source, data.inbound_address), ...(evm ? { router: config.thorchain.router } : {}),
|
|
106
|
+
expirySeconds: integer(data.expiry), gasPrice: gasPrice.toString(),
|
|
107
|
+
streaming: true, streamingInterval: config.thorchain.streamingInterval, streamingQuantity: config.thorchain.streamingQuantity,
|
|
108
|
+
memo: `=:${destination.thorchain.asset}:${input.to}/${input.from}:${BigInt(minimumBaseUnits) / destination.thorchain.scale}/${config.thorchain.streamingInterval}/${config.thorchain.streamingQuantity}`
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async prepare(quote) {
|
|
113
|
+
const plan = {
|
|
114
|
+
depositAddress: assetAddress(this.route.source, quote.details.depositAddress),
|
|
115
|
+
...(this.route.source.network.family === 'evm' ? { router: config.thorchain.router } : {}),
|
|
116
|
+
expiresAt: quote.expiresAt, expirySeconds: integer(quote.details.expirySeconds),
|
|
117
|
+
memo: thorMemo(quote, this.route)
|
|
118
|
+
};
|
|
119
|
+
await this.validate(quote, plan);
|
|
120
|
+
return plan;
|
|
121
|
+
}
|
|
122
|
+
async validate(quote, plan) {
|
|
123
|
+
assertTime(Math.min(quote.expiresAt, plan.expiresAt));
|
|
124
|
+
const inbound = await this.inbound();
|
|
125
|
+
if (quote.routeId !== this.route.id || !sameAssetAddress(this.route.source, inbound.address, plan.depositAddress) ||
|
|
126
|
+
(this.route.source.network.family === 'evm' && plan.router !== config.thorchain.router) ||
|
|
127
|
+
plan.memo !== thorMemo(quote, this.route)) {
|
|
128
|
+
throw new Error('THORChain vault or swap parameters changed; obtain a new quote.');
|
|
129
|
+
}
|
|
130
|
+
if (this.route.source.network.family === 'bitcoin' &&
|
|
131
|
+
(BigInt(quote.amountBaseUnits) <= units(inbound.dust_threshold) || units(inbound.gas_rate) > BigInt(quote.funding.rate))) {
|
|
132
|
+
throw new Error('THORChain Bitcoin funding requirements changed; obtain a new quote.');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async status(operation) {
|
|
136
|
+
const { source, destination } = this.route;
|
|
137
|
+
if (operation.quote.routeId !== this.route.id)
|
|
138
|
+
throw new Error('Swap route mismatch.');
|
|
139
|
+
const deposit = operation.steps.find(step => step.kind === 'deposit');
|
|
140
|
+
if (!deposit)
|
|
141
|
+
return { state: 'deposit_pending' };
|
|
142
|
+
let data;
|
|
143
|
+
try {
|
|
144
|
+
data = record(await swapJson(`${config.thorchain.url}/thorchain/tx/status/${transactionHash(deposit.hash)}`));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error instanceof SwapHttpError && error.status === 404)
|
|
148
|
+
return { state: 'deposit_pending' };
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
if (!data.tx)
|
|
152
|
+
return { state: 'deposit_pending' };
|
|
153
|
+
const tx = record(data.tx);
|
|
154
|
+
if (transactionHash(tx.id) !== transactionHash(deposit.hash) || tx.chain !== source.thorchain.chain ||
|
|
155
|
+
!sameAssetAddress(source, tx.from_address, operation.quote.from) || tx.memo !== operation.plan?.memo) {
|
|
156
|
+
throw new Error('THORChain deposit does not match this swap.');
|
|
157
|
+
}
|
|
158
|
+
const coins = list(tx.coins).map(record);
|
|
159
|
+
if (coins.length !== 1 || textField(coins[0].asset).toUpperCase() !== source.thorchain.asset ||
|
|
160
|
+
units(coins[0].amount) * source.thorchain.scale !== BigInt(operation.quote.amountBaseUnits)) {
|
|
161
|
+
throw new Error('THORChain deposit asset or amount mismatch.');
|
|
162
|
+
}
|
|
163
|
+
const outputs = data.out_txs == null ? [] : list(data.out_txs).map(record);
|
|
164
|
+
const planned = data.planned_out_txs == null ? [] : list(data.planned_out_txs).map(record);
|
|
165
|
+
const stages = record(data.stages);
|
|
166
|
+
const swapStatus = stages.swap_status ? record(stages.swap_status) : null;
|
|
167
|
+
const progress = {
|
|
168
|
+
state: 'deposit_confirmed',
|
|
169
|
+
settlementComplete: swapStatus?.pending === false && !!stages.outbound_signed && record(stages.outbound_signed).completed === true &&
|
|
170
|
+
planned.length > 0 && outputs.length === planned.length
|
|
171
|
+
};
|
|
172
|
+
for (const output of outputs) {
|
|
173
|
+
const values = list(output.coins).map(record);
|
|
174
|
+
for (const coin of values) {
|
|
175
|
+
if (output.chain === destination.thorchain.chain && textField(coin.asset).toUpperCase() === destination.thorchain.asset && sameAssetAddress(destination, output.to_address, operation.quote.to)) {
|
|
176
|
+
if (progress.payoutHash)
|
|
177
|
+
throw new Error('Unexpected multiple payouts.');
|
|
178
|
+
progress.payoutHash = transactionHash(output.id, destination.network.family === 'evm');
|
|
179
|
+
progress.payoutBaseUnits = (units(coin.amount) * destination.thorchain.scale).toString();
|
|
180
|
+
progress.state = 'payout_pending';
|
|
181
|
+
}
|
|
182
|
+
else if (output.chain === source.thorchain.chain && textField(coin.asset).toUpperCase() === source.thorchain.asset &&
|
|
183
|
+
sameAssetAddress(source, output.to_address, operation.quote.from) && textField(output.memo).startsWith('REFUND:')) {
|
|
184
|
+
if (progress.refundHash)
|
|
185
|
+
throw new Error('Unexpected multiple refunds.');
|
|
186
|
+
progress.refundHash = transactionHash(output.id, source.network.family === 'evm');
|
|
187
|
+
progress.refundBaseUnits = (units(coin.amount) * source.thorchain.scale).toString();
|
|
188
|
+
progress.state = 'refund_pending';
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
throw new Error('Unexpected THORChain payout asset or destination.');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (progress.refundHash || planned.some(item => item.refund === true)) {
|
|
196
|
+
try {
|
|
197
|
+
const history = record(await swapJson(`${config.thorchain.midgardUrl}/v2/actions?txid=${transactionHash(deposit.hash)}`));
|
|
198
|
+
const action = list(history.actions).map(record).find(item => item.type === 'refund' &&
|
|
199
|
+
list(item.in).map(record).some(input => transactionHash(input.txID) === transactionHash(deposit.hash)));
|
|
200
|
+
if (action) {
|
|
201
|
+
const reason = textField(record(record(action.metadata).refund).reason).replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 240);
|
|
202
|
+
progress.message = `THORChain refund: ${reason}.`;
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
progress.message = 'THORChain refund reason is not available yet.';
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
progress.message = 'THORChain refund reason is temporarily unavailable.';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (swapStatus?.pending === true) {
|
|
212
|
+
progress.state = 'swapping';
|
|
213
|
+
if (swapStatus.streaming) {
|
|
214
|
+
const streaming = record(swapStatus.streaming);
|
|
215
|
+
progress.message = [progress.message, `Streaming: ${integer(streaming.count)} / ${integer(streaming.quantity)} swap attempts.`].filter(Boolean).join(' ');
|
|
216
|
+
}
|
|
217
|
+
return progress;
|
|
218
|
+
}
|
|
219
|
+
if (progress.payoutHash || progress.refundHash)
|
|
220
|
+
return progress;
|
|
221
|
+
if (planned.some(item => item.refund === true))
|
|
222
|
+
return { ...progress, state: 'refund_pending' };
|
|
223
|
+
if (planned.length)
|
|
224
|
+
return { ...progress, state: 'payout_pending' };
|
|
225
|
+
if (stages.swap_finalised && record(stages.swap_finalised).completed === true)
|
|
226
|
+
return { state: 'payout_pending' };
|
|
227
|
+
if (stages.inbound_finalised && record(stages.inbound_finalised).completed === true)
|
|
228
|
+
return { state: 'swapping' };
|
|
229
|
+
return progress;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export class ChainflipSwapProvider {
|
|
233
|
+
route;
|
|
234
|
+
id = 'chainflip';
|
|
235
|
+
constructor(route = swapRoutes['bsc-btc']) {
|
|
236
|
+
this.route = route;
|
|
237
|
+
}
|
|
238
|
+
async available() {
|
|
239
|
+
const info = record(await swapJson(`${config.chainflip.url}/api/networkInfo`));
|
|
240
|
+
const assets = list(info.assets).map(record);
|
|
241
|
+
const source = assets.find(asset => asset.asset === this.route.source.chainflip.id);
|
|
242
|
+
const destination = assets.find(asset => asset.asset === this.route.destination.chainflip.id);
|
|
243
|
+
for (const [entry, asset] of [[source, this.route.source], [destination, this.route.destination]]) {
|
|
244
|
+
if (!entry)
|
|
245
|
+
throw new Error(`Chainflip ${asset.label} is currently unavailable: asset not listed by the provider API.`);
|
|
246
|
+
}
|
|
247
|
+
for (const [entry, asset, field, action] of [
|
|
248
|
+
[source, this.route.source, 'depositChannelCreationEnabled', 'deposit channel creation'],
|
|
249
|
+
[source, this.route.source, 'depositChannelDepositsEnabled', 'deposits'],
|
|
250
|
+
[destination, this.route.destination, 'egressEnabled', 'payouts']
|
|
251
|
+
]) {
|
|
252
|
+
if (entry[field] !== true) {
|
|
253
|
+
const reason = entry[field] === false ? `${action} disabled by the provider` : `${action} status unverified`;
|
|
254
|
+
throw new Error(`Chainflip ${asset.label} is currently unavailable: ${reason}.`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async quote(input) {
|
|
259
|
+
if (input.routeId !== this.route.id)
|
|
260
|
+
throw new Error('Swap route mismatch.');
|
|
261
|
+
await this.available();
|
|
262
|
+
const params = new URLSearchParams({
|
|
263
|
+
srcChain: this.route.source.chainflip.chain, srcAsset: this.route.source.asset, destChain: this.route.destination.chainflip.chain, destAsset: this.route.destination.asset,
|
|
264
|
+
amount: input.amountBaseUnits, isVaultSwap: 'false', isOnChain: 'false', dcaV2Enabled: 'false'
|
|
265
|
+
});
|
|
266
|
+
const quotes = list(await swapJson(`${config.chainflip.url}/v2/quote?${params}`)).map(record);
|
|
267
|
+
const regular = quotes.filter(item => item.type === 'REGULAR' && !item.dcaParams && !item.ccmParams &&
|
|
268
|
+
!item.isVaultSwap && !item.isOnChain && !item.maxBoostFeeBps && item.lowLiquidityWarning !== true);
|
|
269
|
+
if (!regular.length)
|
|
270
|
+
throw new Error('Chainflip has no safe single-execution quote.');
|
|
271
|
+
const data = regular.sort((a, b) => units(a.egressAmount) > units(b.egressAmount) ? -1 : 1)[0];
|
|
272
|
+
const source = record(data.srcAsset);
|
|
273
|
+
const target = record(data.destAsset);
|
|
274
|
+
if (source.chain !== this.route.source.chainflip.chain || source.asset !== this.route.source.asset ||
|
|
275
|
+
target.chain !== this.route.destination.chainflip.chain || target.asset !== this.route.destination.asset ||
|
|
276
|
+
units(data.depositAmount) !== BigInt(input.amountBaseUnits))
|
|
277
|
+
throw new Error('Chainflip quote asset or amount mismatch.');
|
|
278
|
+
const fees = [];
|
|
279
|
+
let egress = 0n;
|
|
280
|
+
const feeEntries = [
|
|
281
|
+
...list(data.includedFees),
|
|
282
|
+
...list(data.poolInfo).map(pool => ({ ...record(record(pool).fee), type: 'LIQUIDITY' }))
|
|
283
|
+
];
|
|
284
|
+
for (const value of feeEntries) {
|
|
285
|
+
const fee = record(value);
|
|
286
|
+
const amount = units(fee.amount);
|
|
287
|
+
if ((fee.type === 'BOOST' || fee.type === 'BROKER') && amount !== 0n)
|
|
288
|
+
throw new Error('Unexpected Chainflip extra fee.');
|
|
289
|
+
const asset = textField(fee.asset);
|
|
290
|
+
const chain = textField(fee.chain);
|
|
291
|
+
const decimals = asset === 'BTC' && chain === 'Bitcoin' ? 8 :
|
|
292
|
+
asset === 'USDT' && chain === 'Bsc' ? 18 :
|
|
293
|
+
(asset === 'USDC' || asset === 'USDT') && chain === 'Ethereum' ? 6 : null;
|
|
294
|
+
if (decimals === null)
|
|
295
|
+
throw new Error('Unsupported Chainflip fee denomination.');
|
|
296
|
+
fees.push({ label: textField(fee.type), asset: `${asset} (${chain})`, amountBaseUnits: amount.toString(), decimals });
|
|
297
|
+
if (fee.type === 'EGRESS') {
|
|
298
|
+
if (asset !== this.route.destination.asset || chain !== this.route.destination.chainflip.chain)
|
|
299
|
+
throw new Error('Invalid Chainflip egress fee.');
|
|
300
|
+
egress += amount;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const expected = units(data.egressAmount);
|
|
304
|
+
const minimumBaseUnits = minimumOutput(expected);
|
|
305
|
+
const [price, scale] = decimalRatio(data.estimatedPrice);
|
|
306
|
+
const minPriceX128 = ceilDiv(price * (BigInt(minimumBaseUnits) + egress) * (1n << 128n) * 10n ** BigInt(this.route.destination.decimals), scale * (expected + egress) * 10n ** BigInt(this.route.source.decimals));
|
|
307
|
+
if (minPriceX128 <= 0n)
|
|
308
|
+
throw new Error('Invalid Chainflip minimum price.');
|
|
309
|
+
return {
|
|
310
|
+
provider: this.id, expectedBaseUnits: expected.toString(), minimumBaseUnits,
|
|
311
|
+
expiresAt: Date.now() + config.quoteLifetimeMs,
|
|
312
|
+
estimatedSeconds: integer(data.estimatedDurationSeconds), fees,
|
|
313
|
+
details: { quote: data, minPriceX128: minPriceX128.toString() }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async prepare(quote) {
|
|
317
|
+
assertTime(quote.expiresAt);
|
|
318
|
+
await this.available();
|
|
319
|
+
const data = record(await swapJson(`${config.chainflip.url}/api/openSwapDepositChannel`, {
|
|
320
|
+
srcAsset: { chain: this.route.source.chainflip.chain, asset: this.route.source.asset },
|
|
321
|
+
destAsset: { chain: this.route.destination.chainflip.chain, asset: this.route.destination.asset },
|
|
322
|
+
srcAddress: quote.from, destAddress: quote.to, amount: quote.amountBaseUnits,
|
|
323
|
+
maxBoostFeeBps: 0, takeCommission: false,
|
|
324
|
+
fillOrKillParams: {
|
|
325
|
+
refundAddress: quote.from, retryDurationBlocks: config.chainflip.refundRetryBlocks,
|
|
326
|
+
minPriceX128: textField(quote.details.minPriceX128), maxOraclePriceSlippage: null
|
|
327
|
+
},
|
|
328
|
+
quote: record(quote.details.quote)
|
|
329
|
+
}));
|
|
330
|
+
if (integer(data.brokerCommissionBps) !== 0 || integer(data.maxBoostFeeBps) !== 0 || units(data.channelOpeningFee) !== 0n) {
|
|
331
|
+
throw new Error('Chainflip channel introduced an unapproved fee; no funds sent.');
|
|
332
|
+
}
|
|
333
|
+
const expiresAt = integer(data.estimatedExpiryTime);
|
|
334
|
+
const plan = {
|
|
335
|
+
depositAddress: assetAddress(this.route.source, data.depositAddress), providerId: textField(data.id),
|
|
336
|
+
expiresAt: Math.min(quote.expiresAt, expiresAt), channelExpiryBlock: units(data.srcChainExpiryBlock).toString()
|
|
337
|
+
};
|
|
338
|
+
assertTime(plan.expiresAt);
|
|
339
|
+
return plan;
|
|
340
|
+
}
|
|
341
|
+
checkStatus(data, quote, plan) {
|
|
342
|
+
if (quote.routeId !== this.route.id || data.srcChain !== this.route.source.chainflip.chain || data.srcAsset !== this.route.source.asset ||
|
|
343
|
+
data.destChain !== this.route.destination.chainflip.chain || data.destAsset !== this.route.destination.asset ||
|
|
344
|
+
!sameAssetAddress(this.route.destination, data.destAddress, quote.to) || data.dcaParams || data.ccmParams) {
|
|
345
|
+
throw new Error('Chainflip channel parameters do not match the accepted swap.');
|
|
346
|
+
}
|
|
347
|
+
const channel = record(data.depositChannel);
|
|
348
|
+
if (channel.id !== plan.providerId || !sameAssetAddress(this.route.source, channel.depositAddress, plan.depositAddress) ||
|
|
349
|
+
units(channel.expectedDepositAmount) !== BigInt(quote.amountBaseUnits) || channel.dcaParams) {
|
|
350
|
+
throw new Error('Chainflip deposit channel mismatch.');
|
|
351
|
+
}
|
|
352
|
+
const protection = record(data.fillOrKillParams);
|
|
353
|
+
if (!sameAssetAddress(this.route.source, protection.refundAddress, quote.from))
|
|
354
|
+
throw new Error('Chainflip refund address mismatch.');
|
|
355
|
+
const [price, scale] = decimalRatio(protection.minPrice);
|
|
356
|
+
const actual = ceilDiv(price * (1n << 128n) * 10n ** BigInt(this.route.destination.decimals), scale * 10n ** BigInt(this.route.source.decimals));
|
|
357
|
+
if (actual + 1n < units(quote.details.minPriceX128))
|
|
358
|
+
throw new Error('Chainflip price protection is weaker than accepted.');
|
|
359
|
+
if (integer(protection.retryDurationBlocks) !== config.chainflip.refundRetryBlocks)
|
|
360
|
+
throw new Error('Chainflip refund deadline mismatch.');
|
|
361
|
+
if (data.boost && units(String(record(data.boost).maxBoostFeeBps)) !== 0n)
|
|
362
|
+
throw new Error('Unexpected boosted swap.');
|
|
363
|
+
}
|
|
364
|
+
async validate(quote, plan) {
|
|
365
|
+
assertTime(Math.min(quote.expiresAt, plan.expiresAt));
|
|
366
|
+
await this.available();
|
|
367
|
+
const data = record(await swapJson(`${config.chainflip.url}/v2/swaps/${encodeURIComponent(textField(plan.providerId))}`));
|
|
368
|
+
this.checkStatus(data, quote, plan);
|
|
369
|
+
if (data.state !== 'WAITING' || record(data.depositChannel).isExpired !== false)
|
|
370
|
+
throw new Error('Chainflip channel is no longer waiting for this deposit.');
|
|
371
|
+
}
|
|
372
|
+
async status(operation) {
|
|
373
|
+
if (!operation.plan?.providerId)
|
|
374
|
+
return { state: 'deposit_pending' };
|
|
375
|
+
const data = record(await swapJson(`${config.chainflip.url}/v2/swaps/${encodeURIComponent(operation.plan.providerId)}`));
|
|
376
|
+
this.checkStatus(data, operation.quote, operation.plan);
|
|
377
|
+
const progress = { state: 'deposit_pending' };
|
|
378
|
+
if (data.deposit) {
|
|
379
|
+
const deposit = record(data.deposit);
|
|
380
|
+
if (units(deposit.amount) !== BigInt(operation.quote.amountBaseUnits))
|
|
381
|
+
throw new Error('Chainflip deposit amount mismatch.');
|
|
382
|
+
if (deposit.txRef && transactionHash(deposit.txRef, this.route.source.network.family === 'evm') !== operation.steps.find(step => step.kind === 'deposit')?.hash) {
|
|
383
|
+
throw new Error('Chainflip deposit transaction mismatch.');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (data.state === 'SWAPPING')
|
|
387
|
+
progress.state = 'swapping';
|
|
388
|
+
else if (data.state === 'RECEIVING')
|
|
389
|
+
progress.state = 'deposit_confirmed';
|
|
390
|
+
else if (['SENDING', 'SENT', 'COMPLETED'].includes(textField(data.state)))
|
|
391
|
+
progress.state = 'payout_pending';
|
|
392
|
+
else if (data.state === 'FAILED') {
|
|
393
|
+
progress.state = 'needs_attention';
|
|
394
|
+
progress.message = 'Provider reports a failure. Funds are not marked refunded until the source-asset refund is verified.';
|
|
395
|
+
}
|
|
396
|
+
else if (data.state !== 'WAITING')
|
|
397
|
+
throw new Error('Unknown Chainflip swap state.');
|
|
398
|
+
if (data.swapEgress) {
|
|
399
|
+
const egress = record(data.swapEgress);
|
|
400
|
+
progress.state = 'payout_pending';
|
|
401
|
+
if (egress.txRef) {
|
|
402
|
+
progress.payoutHash = transactionHash(egress.txRef, this.route.destination.network.family === 'evm');
|
|
403
|
+
progress.payoutBaseUnits = units(egress.amount).toString();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (data.refundEgress) {
|
|
407
|
+
const refund = record(data.refundEgress);
|
|
408
|
+
progress.state = 'refund_pending';
|
|
409
|
+
if (refund.txRef) {
|
|
410
|
+
progress.refundHash = transactionHash(refund.txRef, this.route.source.network.family === 'evm');
|
|
411
|
+
progress.refundBaseUnits = units(refund.amount).toString();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (data.fallbackEgress) {
|
|
415
|
+
progress.state = 'needs_attention';
|
|
416
|
+
progress.message = 'Provider reports an unexpected fallback payout. Check the operation before taking action.';
|
|
417
|
+
}
|
|
418
|
+
return progress;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
export function createSwapProviders(route = swapRoutes['bsc-btc']) {
|
|
422
|
+
return [new ChainflipSwapProvider(route), new ThorchainSwapProvider(route)];
|
|
423
|
+
}
|
|
424
|
+
//# sourceMappingURL=providers.js.map
|