Skip to content

Commit

Permalink
Merge pull request #3965 from osmosis-labs/stage
Browse files Browse the repository at this point in the history
Publish Stage
  • Loading branch information
JoseRFelix authored Nov 21, 2024
2 parents f8f2d4b + 8419dea commit 726eb17
Show file tree
Hide file tree
Showing 26 changed files with 771 additions and 317 deletions.
2 changes: 1 addition & 1 deletion packages/bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"launchdarkly-node-client-sdk": "^3.3.0",
"long": "^5.2.3",
"lru-cache": "^10.0.1",
"nomic-bitcoin": "^4.2.0",
"nomic-bitcoin": "^5.0.0-pre.0",
"viem": "^2.21.19",
"zod": "^3.22.4"
},
Expand Down
155 changes: 131 additions & 24 deletions packages/bridge/src/nomic/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import type { Registry } from "@cosmjs/proto-signing";
import { Dec, RatePretty } from "@keplr-wallet/unit";
import { getRouteTokenOutGivenIn } from "@osmosis-labs/server";
import { estimateGasFee, getSwapMessages } from "@osmosis-labs/tx";
import {
estimateGasFee,
getSwapMessages,
makeSkipIbcHookSwapMemo,
SkipSwapIbcHookContractAddress,
} from "@osmosis-labs/tx";
import { IbcTransferMethod } from "@osmosis-labs/types";
import {
deriveCosmosAddress,
getAllBtcMinimalDenom,
getnBTCMinimalDenom,
isCosmosAddressValid,
timeout,
} from "@osmosis-labs/utils";
import {
BaseDepositOptions,
buildDestination,
DepositInfo,
generateDepositAddressIbc,
getPendingDeposits,
IbcDepositOptions,
} from "nomic-bitcoin";

import { BridgeQuoteError } from "../errors";
Expand Down Expand Up @@ -40,13 +50,15 @@ export class NomicBridgeProvider implements BridgeProvider {

readonly relayers: string[];
readonly nBTCMinimalDenom: string;
readonly allBtcMinimalDenom: string | undefined;
protected protoRegistry: Registry | null = null;

constructor(protected readonly ctx: BridgeProviderContext) {
this.nBTCMinimalDenom =
this.ctx.env === "mainnet"
? "ibc/75345531D87BD90BF108BE7240BD721CB2CB0A1F16D4EBA71B09EC3C43E15C8F" // nBTC
: "ibc/72D483F0FD4229DBF3ACC78E648F0399C4ACADDFDBCDD9FE791FEE4443343422"; // Testnet nBTC
this.allBtcMinimalDenom = getAllBtcMinimalDenom({ env: this.ctx.env });
this.nBTCMinimalDenom = getnBTCMinimalDenom({
env: this.ctx.env,
});

this.relayers =
this.ctx.env === "testnet"
? ["https://testnet-relayer.nomic.io:8443"]
Expand Down Expand Up @@ -111,7 +123,9 @@ export class NomicBridgeProvider implements BridgeProvider {
| Awaited<ReturnType<typeof getRouteTokenOutGivenIn>>
| undefined;

if (fromAsset.address !== this.nBTCMinimalDenom) {
if (
fromAsset.address.toLowerCase() === this.allBtcMinimalDenom?.toLowerCase()
) {
swapRoute = await getRouteTokenOutGivenIn({
assetLists: this.ctx.assetLists,
tokenInAmount: fromAmount,
Expand Down Expand Up @@ -264,6 +278,7 @@ export class NomicBridgeProvider implements BridgeProvider {
async getDepositAddress({
fromChain,
toAddress,
toAsset,
}: GetDepositAddressParams): Promise<BridgeDepositAddress> {
if (!isCosmosAddressValid({ address: toAddress, bech32Prefix: "osmo" })) {
throw new BridgeQuoteError({
Expand All @@ -287,11 +302,11 @@ export class NomicBridgeProvider implements BridgeProvider {
});
}

const transferMethod = nomicBtc.transferMethods.find(
const nomicIbcTransferMethod = nomicBtc.transferMethods.find(
(method): method is IbcTransferMethod => method.type === "ibc"
);

if (!transferMethod) {
if (!nomicIbcTransferMethod) {
throw new BridgeQuoteError({
bridgeId: "Nomic",
errorType: "UnsupportedQuoteError",
Expand All @@ -307,11 +322,52 @@ export class NomicBridgeProvider implements BridgeProvider {
});
}

const nomicChain = this.ctx.chainList.find(
({ chain_name }) =>
chain_name === nomicIbcTransferMethod.counterparty.chainName
);

if (!nomicChain) {
throw new BridgeQuoteError({
bridgeId: "Nomic",
errorType: "UnsupportedQuoteError",
message: "Nomic chain not found in chain list.",
});
}

const userWantsAllBtc =
this.allBtcMinimalDenom && toAsset.address === this.allBtcMinimalDenom;

const now = Date.now();
const timeoutTimestampFiveDaysFromNow =
Number(now + 86_400 * 5 * 1_000 - (now % (60 * 60 * 1_000))) * 1_000_000;
const swapMemo = userWantsAllBtc
? makeSkipIbcHookSwapMemo({
denomIn: this.nBTCMinimalDenom,
denomOut:
this.ctx.env === "mainnet" ? this.allBtcMinimalDenom : "uosmo",
env: this.ctx.env,
minAmountOut: "1",
poolId:
this.ctx.env === "mainnet"
? "1868" // nBTC/allBTC pool on Osmosis
: "663", // nBTC/osmo pool on Osmosis. Since there's no alloyed btc in testnet, we'll use these pool instead
receiverOsmoAddress: toAddress,
timeoutTimestamp: timeoutTimestampFiveDaysFromNow,
})
: undefined;

const depositInfo = await generateDepositAddressIbc({
relayers: this.relayers,
channel: transferMethod.counterparty.channelId, // IBC channel ID on Nomic
channel: nomicIbcTransferMethod.counterparty.channelId, // IBC channel ID on Nomic
bitcoinNetwork: this.ctx.env === "testnet" ? "testnet" : "bitcoin",
receiver: toAddress,
sender: deriveCosmosAddress({
address: toAddress,
desiredBech32Prefix: nomicChain.bech32_prefix,
}),
receiver:
userWantsAllBtc && swapMemo ? swapMemo.wasm.contract : toAddress,
...(swapMemo ? { memo: JSON.stringify(swapMemo) } : {}),
});

if (depositInfo.code === 1) {
Expand Down Expand Up @@ -387,18 +443,31 @@ export class NomicBridgeProvider implements BridgeProvider {
const isNomicBtc =
assetListAsset.coinMinimalDenom.toLowerCase() ===
this.nBTCMinimalDenom.toLowerCase();
const isAllBtc =
this.allBtcMinimalDenom &&
assetListAsset.coinMinimalDenom.toLowerCase() ===
this.allBtcMinimalDenom.toLowerCase();

const nomicWithdrawAmountEnabled = await getLaunchDarklyFlagValue({
const nomicWithdrawEnabled = await getLaunchDarklyFlagValue({
key: "nomicWithdrawAmount",
});

if (bitcoinCounterparty || isNomicBtc) {
let transferTypes: BridgeSupportedAsset["transferTypes"] = [];

if (direction === "withdraw" && nomicWithdrawEnabled) {
transferTypes = ["quote"];
} else if (direction === "deposit" && (isNomicBtc || isAllBtc)) {
transferTypes = ["deposit-address"];
}

if (transferTypes.length === 0) {
return [];
}

return [
{
transferTypes:
direction === "withdraw" && nomicWithdrawAmountEnabled
? ["quote"]
: ["deposit-address"],
transferTypes,
chainId: "bitcoin",
chainName: "Bitcoin",
chainType: "bitcoin",
Expand Down Expand Up @@ -437,10 +506,43 @@ export class NomicBridgeProvider implements BridgeProvider {

async getPendingDeposits({ address }: { address: string }) {
try {
const pendingDeposits = await timeout(
() => getPendingDeposits(this.relayers, address),
10000
)();
const [pendingDeposits, skipSwapPendingDeposits] = await Promise.all([
timeout(() => getPendingDeposits(this.relayers, address), 10000)(),
/**
* We need to check all deposits to skip contract since we set the receiver to the skip contract address.
* So, we need to filter out any deposits that are not intended for the user.
*/
timeout(
() =>
getPendingDeposits(this.relayers, SkipSwapIbcHookContractAddress),
10000
)(),
]);

/**
* Filter out deposits that are not intended for the user
*/
const filteredSkipSwapPendingDeposits = skipSwapPendingDeposits
.filter((deposit) => {
try {
if (!("dest" in deposit)) return false;
const dest = deposit.dest as {
data: BaseDepositOptions & IbcDepositOptions;
};
const memo = JSON.parse(dest.data.memo ?? "{}");
return (
memo.wasm.msg.swap_and_action.post_swap_action.transfer
.to_address === address
);
} catch (error) {
console.error("Error parsing memo:", error);
return false;
}
})
.map((deposit) => ({
...deposit,
__type: "contract-deposit" as const,
}));

const nomicBtc = this.ctx.assetLists
.flatMap(({ assets }) => assets)
Expand All @@ -452,24 +554,29 @@ export class NomicBridgeProvider implements BridgeProvider {
throw new Error("Nomic Bitcoin asset not found in asset list.");
}

return pendingDeposits.map((deposit) => ({
const deposits = [
...pendingDeposits,
...filteredSkipSwapPendingDeposits,
] as (DepositInfo & { __type?: "contract-deposit" })[];

return deposits.map((deposit) => ({
transactionId: deposit.txid,
amount: deposit.amount,
confirmations: deposit.confirmations,
networkFee: {
address: nomicBtc.coinMinimalDenom,
amount: ((deposit.minerFeeRate ?? 0) * 1e8).toString(),
decimals: 8,
// TODO: Handle case when we can receive allBTC
denom: nomicBtc.symbol,
denom:
deposit.__type === "contract-deposit" ? "BTC" : nomicBtc.symbol,
coinGeckoId: nomicBtc.coingeckoId,
},
providerFee: {
address: nomicBtc.coinMinimalDenom,
amount: ((deposit.bridgeFeeRate ?? 0) * deposit.amount).toString(),
decimals: 8,
// TODO: Handle case when we can receive allBTC
denom: nomicBtc.symbol,
denom:
deposit.__type === "contract-deposit" ? "BTC" : nomicBtc.symbol,
coinGeckoId: nomicBtc.coingeckoId,
},
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,19 @@ describe("Has Asset Variants", () => {
denom:
"ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", // USDC
amount: "0.2",
fiatValue: "0.2",
},
{
denom:
"factory/osmo1rckme96ptawr4zwexxj5g5gej9s2dmud8r2t9j0k0prn5mch5g4snzzwjv/sail", // SAIL
amount: "0.2",
fiatValue: "0.2",
},
{
denom:
"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", // ETH.axl <- this is the variant
amount: "0.2",
fiatValue: "0.2",
},
];

Expand All @@ -284,11 +287,13 @@ describe("Has Asset Variants", () => {
{
denom: "uosmo", // OSMO
amount: "0.2",
fiatValue: "0.2",
},
{
denom:
"factory/osmo1rckme96ptawr4zwexxj5g5gej9s2dmud8r2t9j0k0prn5mch5g4snzzwjv/sail", // SAIL
amount: "0.2",
fiatValue: "0.2",
},
];

Expand All @@ -297,7 +302,11 @@ describe("Has Asset Variants", () => {
});

it("should return empty array when user has no asset variants", () => {
const userCoinMinimalDenoms: { denom: string; amount: string }[] = [];
const userCoinMinimalDenoms: {
denom: string;
amount: string;
fiatValue: string;
}[] = [];

const result = checkAssetVariants(userCoinMinimalDenoms, assetLists);
expect(result.length).toBe(0);
Expand Down
Loading

0 comments on commit 726eb17

Please sign in to comment.