mirror of
https://github.com/orionprotocol/sdk.git
synced 2026-03-14 06:02:36 +03:00
Added bridge swap
This commit is contained in:
21
README.md
21
README.md
@@ -34,6 +34,7 @@ Orion’s SDK is free to use and does not require an API key or registration. Re
|
||||
- [Get assets](#get-assets)
|
||||
- [Get pairs](#get-pairs)
|
||||
- [Get Orion Bridge history](#get-orion-bridge-history)
|
||||
- [Bridge swap](#bridge-swap)
|
||||
- [Withdraw](#withdraw)
|
||||
- [Deposit](#deposit)
|
||||
- [Get swap info](#get-swap-info)
|
||||
@@ -149,6 +150,26 @@ const bridgeHistory = await orion.bridge.getHistory(
|
||||
);
|
||||
```
|
||||
|
||||
### Bridge swap
|
||||
|
||||
```ts
|
||||
const orion = new Orion("production");
|
||||
const wallet = new Wallet(privateKey);
|
||||
|
||||
orion.bridge.swap(
|
||||
"ORN", // Asset name
|
||||
0.12345678, // Amount
|
||||
SupportedChainId.FANTOM_OPERA,
|
||||
SupportedChainId.BSC,
|
||||
wallet,
|
||||
{
|
||||
autoApprove: true,
|
||||
logger: console.log,
|
||||
withdrawToWallet: true, // Enable withdraw to wallet
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Withdraw
|
||||
|
||||
```ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@orionprotocol/sdk",
|
||||
"version": "0.17.21",
|
||||
"version": "0.17.22",
|
||||
"description": "Orion Protocol SDK",
|
||||
"main": "./lib/esm/index.js",
|
||||
"module": "./lib/esm/index.js",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ethers } from 'ethers';
|
||||
import type OrionUnit from '../OrionUnit';
|
||||
import simpleFetch from '../simpleFetch';
|
||||
import type { SupportedChainId } from '../types';
|
||||
import { isValidChainId } from '../utils';
|
||||
import type OrionUnit from '../../OrionUnit';
|
||||
import simpleFetch from '../../simpleFetch';
|
||||
import type { SupportedChainId } from '../../types';
|
||||
import { isValidChainId } from '../../utils';
|
||||
import ObjectID from 'bson-objectid';
|
||||
|
||||
const getBridgeHistory = async (units: OrionUnit[], address: string, limit = 1000) => {
|
||||
const getHistory = async (units: OrionUnit[], address: string, limit = 1000) => {
|
||||
if (!ethers.utils.isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
||||
const data = await Promise.all(units.map(async ({ orionBlockchain, orionAggregator, chainId }) => {
|
||||
const sourceNetworkHistory = await simpleFetch(orionBlockchain.getSourceAtomicSwapHistory)({
|
||||
@@ -209,4 +209,4 @@ const getBridgeHistory = async (units: OrionUnit[], address: string, limit = 100
|
||||
return aggregatedData;
|
||||
}
|
||||
|
||||
export default getBridgeHistory;
|
||||
export default getHistory;
|
||||
329
src/Orion/bridge/swap.ts
Normal file
329
src/Orion/bridge/swap.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { Exchange__factory } from '@orionprotocol/contracts';
|
||||
import getBalances from '../../utils/getBalances';
|
||||
import BalanceGuard from '../../BalanceGuard';
|
||||
import getAvailableSources from '../../utils/getAvailableFundsSources';
|
||||
import {
|
||||
INTERNAL_ORION_PRECISION,
|
||||
NATIVE_CURRENCY_PRECISION,
|
||||
LOCKATOMIC_GAS_LIMIT,
|
||||
REDEEMATOMIC_GAS_LIMIT,
|
||||
WITHDRAW_GAS_LIMIT
|
||||
} from '../../constants';
|
||||
import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency';
|
||||
import simpleFetch from '../../simpleFetch';
|
||||
import { denormalizeNumber, generateSecret, normalizeNumber, toUpperCase } from '../../utils';
|
||||
import type { SupportedChainId } from '../../types';
|
||||
import type Orion from '..';
|
||||
import type { z } from 'zod';
|
||||
import type { placeAtomicSwapSchema } from '../../services/OrionAggregator/schemas';
|
||||
|
||||
type Params = {
|
||||
assetName: string
|
||||
amount: BigNumber.Value
|
||||
sourceChain: SupportedChainId
|
||||
targetChain: SupportedChainId
|
||||
signer: ethers.Signer
|
||||
orion: Orion
|
||||
options?: {
|
||||
withdrawToWallet?: boolean // By default, the transfer amount remains in the exchange contract
|
||||
autoApprove?: boolean
|
||||
logger?: (message: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
export default async function swap({
|
||||
amount,
|
||||
assetName,
|
||||
sourceChain,
|
||||
targetChain,
|
||||
signer,
|
||||
options,
|
||||
orion
|
||||
}: Params) {
|
||||
const startProcessingTime = Date.now();
|
||||
if (amount === '') throw new Error('Amount can not be empty');
|
||||
if (assetName === '') throw new Error('AssetIn can not be empty');
|
||||
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error(`Amount '${amountBN.toString()}' is not a number`);
|
||||
if (amountBN.lte(0)) throw new Error(`Amount '${amountBN.toString()}' should be greater than 0`);
|
||||
|
||||
const sourceChainOrionUnit = orion.getUnit(sourceChain);
|
||||
const targetChainOrionUnit = orion.getUnit(targetChain);
|
||||
|
||||
const {
|
||||
orionBlockchain: sourceOrionBlockchain,
|
||||
orionAggregator: sourceOrionAggregator,
|
||||
provider: sourceProvider,
|
||||
chainId,
|
||||
} = sourceChainOrionUnit;
|
||||
|
||||
const {
|
||||
orionAggregator: targetOrionAggregator,
|
||||
orionBlockchain: targetOrionBlockchain,
|
||||
provider: targetProvider,
|
||||
} = targetChainOrionUnit;
|
||||
|
||||
const sourceSupportedBridgeAssets = await simpleFetch(sourceOrionBlockchain.getAtomicSwapAssets)();
|
||||
const targetSupportedBridgeAssets = await simpleFetch(targetOrionBlockchain.getAtomicSwapAssets)();
|
||||
|
||||
const commonSupportedBridgeAssets = sourceSupportedBridgeAssets.filter((asset) => targetSupportedBridgeAssets.includes(asset));
|
||||
if (!sourceSupportedBridgeAssets.includes(assetName) || !targetSupportedBridgeAssets.includes(assetName)) {
|
||||
throw new Error(`Asset '${assetName}' not available for swap between ${sourceChain} and ${targetChain} chains. Available assets: ${commonSupportedBridgeAssets.join(', ')}`);
|
||||
}
|
||||
|
||||
const brokersBalances = await new Promise<Partial<Record<string, number>>>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Can't get brokers balances. Timeout"));
|
||||
}, 10000);
|
||||
const id = targetOrionAggregator.ws.subscribe('btasabus', {
|
||||
callback: (data) => {
|
||||
targetOrionAggregator.ws.unsubscribe(id);
|
||||
targetOrionAggregator.ws.destroy();
|
||||
clearTimeout(timeout);
|
||||
resolve(data);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
options?.logger?.(`Wallet address is ${walletAddress}`);
|
||||
|
||||
const assetBrokersBalance = brokersBalances[assetName];
|
||||
if (assetBrokersBalance === undefined) throw new Error(`Asset '${assetName}' not found in brokers balances`);
|
||||
if (assetBrokersBalance === 0) throw new Error(`Asset '${assetName}' is not available for swap`);
|
||||
|
||||
const {
|
||||
exchangeContractAddress: sourceExchangeContractAddress,
|
||||
assetToAddress: sourceAssetToAddress,
|
||||
} = await simpleFetch(sourceOrionBlockchain.getInfo)();
|
||||
|
||||
const sourceChainNativeCryptocurrency = getNativeCryptocurrency(sourceAssetToAddress);
|
||||
const sourceExchangeContract = Exchange__factory.connect(sourceExchangeContractAddress, sourceProvider);
|
||||
const sourceChainGasPriceWei = await simpleFetch(sourceOrionBlockchain.getGasPriceWei)();
|
||||
|
||||
const sourceChainAssetAddress = sourceAssetToAddress[assetName];
|
||||
if (sourceChainAssetAddress === undefined) throw new Error(`Asset '${assetName}' not found in source chain`);
|
||||
|
||||
const {
|
||||
exchangeContractAddress: targetExchangeContractAddress,
|
||||
assetToAddress: targetAssetToAddress,
|
||||
} = await simpleFetch(targetOrionBlockchain.getInfo)();
|
||||
|
||||
const targetChainAssetAddress = targetAssetToAddress[assetName];
|
||||
if (targetChainAssetAddress === undefined) throw new Error(`Asset '${assetName}' not found in target chain`);
|
||||
const targetChainNativeCryptocurrency = getNativeCryptocurrency(targetAssetToAddress);
|
||||
const targetExchangeContract = Exchange__factory.connect(targetExchangeContractAddress, targetProvider);
|
||||
|
||||
const sourceChainBalances = await getBalances(
|
||||
{
|
||||
[assetName]: sourceChainAssetAddress,
|
||||
[sourceChainNativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
sourceOrionAggregator,
|
||||
walletAddress,
|
||||
sourceExchangeContract,
|
||||
sourceProvider,
|
||||
);
|
||||
|
||||
const targetChainBalances = await getBalances(
|
||||
{
|
||||
[assetName]: targetChainAssetAddress,
|
||||
[targetChainNativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
targetOrionAggregator,
|
||||
walletAddress,
|
||||
targetExchangeContract,
|
||||
targetProvider,
|
||||
);
|
||||
|
||||
const sourceChainBalanceGuard = new BalanceGuard(
|
||||
sourceChainBalances,
|
||||
{
|
||||
name: sourceChainNativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
sourceProvider,
|
||||
signer,
|
||||
options?.logger,
|
||||
);
|
||||
|
||||
const targetChainBalanceGuard = new BalanceGuard(
|
||||
targetChainBalances,
|
||||
{
|
||||
name: targetChainNativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
targetProvider,
|
||||
signer,
|
||||
options?.logger,
|
||||
);
|
||||
|
||||
sourceChainBalanceGuard.registerRequirement({
|
||||
reason: 'Amount',
|
||||
asset: {
|
||||
name: assetName,
|
||||
address: sourceChainAssetAddress,
|
||||
},
|
||||
amount: amountBN.toString(),
|
||||
spenderAddress: sourceExchangeContractAddress,
|
||||
sources: getAvailableSources('amount', sourceChainAssetAddress, 'orion_pool'),
|
||||
});
|
||||
|
||||
const amountBlockchainParam = normalizeNumber(
|
||||
amount,
|
||||
INTERNAL_ORION_PRECISION,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
);
|
||||
const secret = generateSecret();
|
||||
const secretHash = ethers.utils.keccak256(secret);
|
||||
options?.logger?.(`Secret is ${secret}`);
|
||||
options?.logger?.(`Secret hash is ${secretHash}`);
|
||||
|
||||
const secondsInDay = 60 * 60 * 24;
|
||||
const expirationDays = 4;
|
||||
const expirationEtherBN = ethers.BigNumber.from(
|
||||
Date.now() + (secondsInDay * expirationDays * 1000),
|
||||
);
|
||||
|
||||
const unsignedLockAtomicTx = await sourceExchangeContract.populateTransaction.lockAtomic({
|
||||
amount: amountBlockchainParam,
|
||||
asset: sourceChainAssetAddress,
|
||||
expiration: expirationEtherBN,
|
||||
secretHash,
|
||||
sender: walletAddress,
|
||||
targetChainId: ethers.BigNumber.from(targetChain),
|
||||
});
|
||||
|
||||
unsignedLockAtomicTx.chainId = parseInt(chainId, 10);
|
||||
unsignedLockAtomicTx.gasPrice = ethers.BigNumber.from(sourceChainGasPriceWei);
|
||||
unsignedLockAtomicTx.from = walletAddress;
|
||||
|
||||
let value = new BigNumber(0);
|
||||
const denormalizedAssetInExchangeBalance = sourceChainBalances[assetName]?.exchange;
|
||||
if (denormalizedAssetInExchangeBalance === undefined) throw new Error(`Asset '${assetName}' exchange balance is not found`);
|
||||
if (assetName === sourceChainNativeCryptocurrency && amountBN.gt(denormalizedAssetInExchangeBalance)) {
|
||||
value = amountBN.minus(denormalizedAssetInExchangeBalance);
|
||||
}
|
||||
unsignedLockAtomicTx.value = normalizeNumber(
|
||||
value.dp(INTERNAL_ORION_PRECISION, BigNumber.ROUND_CEIL),
|
||||
NATIVE_CURRENCY_PRECISION,
|
||||
BigNumber.ROUND_CEIL,
|
||||
);
|
||||
unsignedLockAtomicTx.gasLimit = ethers.BigNumber.from(LOCKATOMIC_GAS_LIMIT);
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(LOCKATOMIC_GAS_LIMIT).mul(sourceChainGasPriceWei);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
sourceChainBalanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: sourceChainNativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: ['wallet']
|
||||
});
|
||||
|
||||
await sourceChainBalanceGuard.check(options?.autoApprove);
|
||||
|
||||
const nonce = await sourceProvider.getTransactionCount(walletAddress, 'pending');
|
||||
unsignedLockAtomicTx.nonce = nonce;
|
||||
|
||||
options?.logger?.('Signing lock tx transaction...');
|
||||
const signedTransaction = await signer.signTransaction(unsignedLockAtomicTx);
|
||||
const lockAtomicTxResponse = await sourceChainOrionUnit.provider.sendTransaction(signedTransaction);
|
||||
options?.logger?.(`Lock tx sent. Tx hash: ${lockAtomicTxResponse.hash}. Waiting for tx to be mined...`);
|
||||
await lockAtomicTxResponse.wait();
|
||||
options?.logger?.('Lock tx mined.');
|
||||
options?.logger?.('Placing atomic swap...');
|
||||
|
||||
const atomicSwap = await new Promise<z.infer<typeof placeAtomicSwapSchema>>((resolve, reject) => {
|
||||
const placeAtomicSwap = () => simpleFetch(targetOrionAggregator.placeAtomicSwap)(
|
||||
secretHash,
|
||||
toUpperCase(sourceChainOrionUnit.networkCode)
|
||||
).then((data) => {
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
resolve(data);
|
||||
}).catch(console.error);
|
||||
const interval = setInterval(() => {
|
||||
placeAtomicSwap().catch(console.error);
|
||||
}, 2000);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Atomic swap placing timeout'));
|
||||
}, 1000 * 60 * 5);
|
||||
});
|
||||
|
||||
options?.logger?.('Atomic swap placed.');
|
||||
|
||||
const targetChainGasPriceWei = await simpleFetch(targetOrionBlockchain.getGasPriceWei)();
|
||||
const unsignedRedeemAtomicTx = await targetExchangeContract.populateTransaction.redeemAtomic(
|
||||
{
|
||||
amount: amountBlockchainParam,
|
||||
asset: targetChainAssetAddress,
|
||||
claimReceiver: atomicSwap.redeemOrder.claimReceiver,
|
||||
expiration: atomicSwap.redeemOrder.expiration,
|
||||
receiver: atomicSwap.redeemOrder.receiver,
|
||||
secretHash: atomicSwap.redeemOrder.secretHash,
|
||||
sender: atomicSwap.redeemOrder.sender,
|
||||
signature: atomicSwap.redeemOrder.signature,
|
||||
},
|
||||
secret
|
||||
)
|
||||
|
||||
unsignedRedeemAtomicTx.chainId = parseInt(targetChain, 10);
|
||||
unsignedRedeemAtomicTx.gasPrice = ethers.BigNumber.from(targetChainGasPriceWei);
|
||||
unsignedRedeemAtomicTx.from = walletAddress;
|
||||
unsignedRedeemAtomicTx.gasLimit = ethers.BigNumber.from(REDEEMATOMIC_GAS_LIMIT);
|
||||
|
||||
const targetTransactionCost = ethers.BigNumber.from(REDEEMATOMIC_GAS_LIMIT).mul(targetChainGasPriceWei);
|
||||
const targetDenormalizedTransactionCost = denormalizeNumber(targetTransactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
targetChainBalanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: targetChainNativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: targetDenormalizedTransactionCost.toString(),
|
||||
sources: ['wallet']
|
||||
});
|
||||
|
||||
await targetChainBalanceGuard.check(options?.autoApprove);
|
||||
|
||||
unsignedRedeemAtomicTx.nonce = await targetProvider.getTransactionCount(walletAddress, 'pending');
|
||||
|
||||
options?.logger?.('Signing redeem tx transaction...');
|
||||
|
||||
const targetSignedTransaction = await signer.signTransaction(unsignedRedeemAtomicTx);
|
||||
const targetLockAtomicTxResponse = await targetChainOrionUnit.provider.sendTransaction(targetSignedTransaction);
|
||||
options?.logger?.(`Redeem tx sent. Tx hash: ${targetLockAtomicTxResponse.hash}. Waiting for tx to be mined...`);
|
||||
|
||||
await targetLockAtomicTxResponse.wait();
|
||||
options?.logger?.('Redeem tx mined.');
|
||||
options?.logger?.('Atomic swap completed.');
|
||||
|
||||
if (options?.withdrawToWallet) {
|
||||
options.logger?.('Withdrawing to wallet...');
|
||||
const unsignedWithdrawTx = await targetExchangeContract.populateTransaction.withdraw(
|
||||
targetChainAssetAddress,
|
||||
amountBlockchainParam,
|
||||
);
|
||||
unsignedWithdrawTx.gasLimit = ethers.BigNumber.from(WITHDRAW_GAS_LIMIT);
|
||||
unsignedWithdrawTx.gasPrice = ethers.BigNumber.from(targetChainGasPriceWei);
|
||||
unsignedWithdrawTx.from = walletAddress;
|
||||
unsignedWithdrawTx.nonce = await targetProvider.getTransactionCount(walletAddress, 'pending');
|
||||
const signedTx = await signer.signTransaction(unsignedWithdrawTx);
|
||||
const withdrawTx = await targetProvider.sendTransaction(signedTx);
|
||||
options.logger?.(`Withdraw tx sent. Tx hash: ${withdrawTx.hash}. Waiting for tx to be mined...`);
|
||||
await withdrawTx.wait();
|
||||
options.logger?.('Withdraw tx mined.');
|
||||
}
|
||||
|
||||
options?.logger?.(`Total processing time: ${Date.now() - startProcessingTime} ms`);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import type { ethers } from 'ethers';
|
||||
import { merge } from 'merge-anything';
|
||||
import { chains, envs } from '../config';
|
||||
import type { networkCodes } from '../constants';
|
||||
@@ -6,7 +8,8 @@ import { ReferralSystem } from '../services/ReferralSystem';
|
||||
import simpleFetch from '../simpleFetch';
|
||||
import type { SupportedChainId, DeepPartial, VerboseOrionUnitConfig, KnownEnv } from '../types';
|
||||
import { isValidChainId } from '../utils';
|
||||
import getBridgeHistory from './getBridgeHistory';
|
||||
import swap from './bridge/swap';
|
||||
import getHistory from './bridge/getHistory';
|
||||
|
||||
type EnvConfig = {
|
||||
analyticsAPI: string
|
||||
@@ -212,6 +215,26 @@ export default class Orion {
|
||||
}
|
||||
|
||||
bridge = {
|
||||
getHistory: (address: string, limit = 1000) => getBridgeHistory(this.unitsArray, address, limit),
|
||||
getHistory: (address: string, limit = 1000) => getHistory(this.unitsArray, address, limit),
|
||||
swap: (
|
||||
assetName: string,
|
||||
amount: BigNumber.Value,
|
||||
sourceChain: SupportedChainId,
|
||||
targetChain: SupportedChainId,
|
||||
signer: ethers.Signer,
|
||||
options: {
|
||||
autoApprove?: boolean
|
||||
logger?: (message: string) => void
|
||||
withdrawToWallet?: boolean
|
||||
}
|
||||
) => swap({
|
||||
amount,
|
||||
assetName,
|
||||
sourceChain,
|
||||
targetChain,
|
||||
signer,
|
||||
orion: this,
|
||||
options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export default class OrionUnit {
|
||||
const intNetwork = parseInt(this.chainId, 10);
|
||||
if (Number.isNaN(intNetwork)) throw new Error('Invalid chainId (not a number)' + this.chainId);
|
||||
this.provider = new ethers.providers.StaticJsonRpcProvider(this.config.nodeJsonRpc, intNetwork);
|
||||
this.provider.pollingInterval = 1000;
|
||||
|
||||
this.orionBlockchain = new OrionBlockchain(this.config.services.orionBlockchain.http);
|
||||
this.orionAggregator = new OrionAggregator(
|
||||
|
||||
Reference in New Issue
Block a user