Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish Stage #3965

Merged
merged 5 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading