Skip to content

Commit

Permalink
Merge pull request #3974 from osmosis-labs/stage
Browse files Browse the repository at this point in the history
Publish Stage
  • Loading branch information
JoseRFelix authored Nov 27, 2024
2 parents 726eb17 + 8427eb3 commit e5e3ff4
Show file tree
Hide file tree
Showing 71 changed files with 3,503 additions and 1,665 deletions.
3 changes: 3 additions & 0 deletions packages/bridge/src/bridge-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IbcBridgeProvider } from "./ibc";
import { BridgeProviderContext } from "./interface";
import { NitroBridgeProvider } from "./nitro";
import { NomicBridgeProvider } from "./nomic";
import { PenumbraBridgeProvider } from "./penumbra";
import { PicassoBridgeProvider } from "./picasso";
import { SkipBridgeProvider } from "./skip";
import { SquidBridgeProvider } from "./squid";
Expand All @@ -21,6 +22,7 @@ export class BridgeProviders {
[WormholeBridgeProvider.ID]: WormholeBridgeProvider;
[NitroBridgeProvider.ID]: NitroBridgeProvider;
[PicassoBridgeProvider.ID]: PicassoBridgeProvider;
[PenumbraBridgeProvider.ID]: PenumbraBridgeProvider;
};

constructor(integratorId: string, commonContext: BridgeProviderContext) {
Expand All @@ -40,6 +42,7 @@ export class BridgeProviders {
[WormholeBridgeProvider.ID]: new WormholeBridgeProvider(commonContext),
[NitroBridgeProvider.ID]: new NitroBridgeProvider(commonContext),
[PicassoBridgeProvider.ID]: new PicassoBridgeProvider(commonContext),
[PenumbraBridgeProvider.ID]: new PenumbraBridgeProvider(commonContext),
};
}
}
8 changes: 8 additions & 0 deletions packages/bridge/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,19 @@ const tronChainSchema = z.object({
chainType: z.literal("tron"),
});

const penumbraChainSchema = z.object({
chainId: z.string(),
chainName: z.string(),
chainType: z.literal("penumbra"),
});

export const bridgeChainSchema = z.discriminatedUnion("chainType", [
cosmosChainSchema,
evmChainSchema,
solanaChainSchema,
bitcoinChainSchema,
tronChainSchema,
penumbraChainSchema,
]);

export type BridgeChain = z.infer<typeof bridgeChainSchema>;
Expand Down Expand Up @@ -510,6 +517,7 @@ const txSnapshotSchema = z.object({
})
),
estimatedArrivalUnix: z.number(),
nomicCheckpointIndex: z.number().optional(),
});

export type TxSnapshot = z.infer<typeof txSnapshotSchema>;
Expand Down
64 changes: 42 additions & 22 deletions packages/bridge/src/nomic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import {
deriveCosmosAddress,
getAllBtcMinimalDenom,
getnBTCMinimalDenom,
getNomicRelayerUrl,
isCosmosAddressValid,
timeout,
} from "@osmosis-labs/utils";
import {
BaseDepositOptions,
buildDestination,
Checkpoint,
DepositInfo,
generateDepositAddressIbc,
getCheckpoint,
getPendingDeposits,
IbcDepositOptions,
} from "nomic-bitcoin";
Expand All @@ -43,9 +46,10 @@ import {
} from "../interface";
import { getGasAsset } from "../utils/gas";
import { getLaunchDarklyFlagValue } from "../utils/launchdarkly";
import { NomicProviderId } from "./utils";

export class NomicBridgeProvider implements BridgeProvider {
static readonly ID = "Nomic";
static readonly ID = NomicProviderId;
readonly providerName = NomicBridgeProvider.ID;

readonly relayers: string[];
Expand All @@ -54,15 +58,11 @@ export class NomicBridgeProvider implements BridgeProvider {
protected protoRegistry: Registry | null = null;

constructor(protected readonly ctx: BridgeProviderContext) {
this.relayers = getNomicRelayerUrl({ env: this.ctx.env });
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"]
: ["https://relayer.nomic.mappum.io:8443"];
}

async getQuote(params: GetBridgeQuoteParams): Promise<BridgeQuote> {
Expand Down Expand Up @@ -182,16 +182,29 @@ export class NomicBridgeProvider implements BridgeProvider {
}),
};

const [ibcTxMessages, estimatedTime] = await Promise.all([
ibcProvider.getTxMessages({
...transactionDataParams,
memo: destMemo,
}),
ibcProvider.estimateTransferTime(
transactionDataParams.fromChain.chainId.toString(),
transactionDataParams.toChain.chainId.toString()
),
]);
const [ibcTxMessages, ibcEstimatedTimeSeconds, nomicCheckpoint] =
await Promise.all([
ibcProvider.getTxMessages({
...transactionDataParams,
memo: destMemo,
}),
ibcProvider.estimateTransferTime(
transactionDataParams.fromChain.chainId.toString(),
transactionDataParams.toChain.chainId.toString()
),
getCheckpoint({
relayers: this.relayers,
bitcoinNetwork: this.ctx.env === "mainnet" ? "bitcoin" : "testnet",
}),
]);

// 4 hours
const nomicEstimatedTimeSeconds = 4 * 60 * 60;

const transferFeeInSats = Math.ceil(
(nomicCheckpoint as Checkpoint & { minerFee: number }).minerFee * 64 + 546
);
const transferFeeInMicroSats = transferFeeInSats * 1e6;

const msgs = [...swapMessages, ...ibcTxMessages];

Expand Down Expand Up @@ -238,22 +251,29 @@ export class NomicBridgeProvider implements BridgeProvider {
...params.fromAsset,
},
expectedOutput: {
amount: !!swapRoute
? swapRoute.amount.toCoin().amount
: params.fromAmount,
amount: (!!swapRoute
? new Dec(swapRoute.amount.toCoin().amount)
: new Dec(params.fromAmount)
)
// Use micro sats because the amount will always be nomic btc which has 14 decimals (micro sats)
.sub(new Dec(transferFeeInMicroSats))
.toString(),
...nomicBridgeAsset,
denom: "BTC",
priceImpact: swapRoute?.priceImpactTokenOut?.toDec().toString() ?? "0",
},
fromChain: params.fromChain,
toChain: params.toChain,
// currently subsidized by relayers, but could be paid by user in future by charging the user the gas cost of
transferFee: {
...params.fromAsset,
denom: "BTC",
chainId: params.fromChain.chainId,
amount: "0",
amount: (params.fromAsset.decimals === 14
? transferFeeInMicroSats
: transferFeeInSats
).toString(),
},
estimatedTime,
estimatedTime: ibcEstimatedTimeSeconds + nomicEstimatedTimeSeconds,
estimatedGasFee: gasFee
? {
address: gasAsset?.address ?? gasFee.denom,
Expand Down
97 changes: 97 additions & 0 deletions packages/bridge/src/nomic/transfer-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Chain } from "@osmosis-labs/types";
import { getNomicRelayerUrl, isNil, poll } from "@osmosis-labs/utils";
import { getCheckpoint } from "nomic-bitcoin";

import type {
BridgeEnvironment,
BridgeTransferStatus,
TransferStatusProvider,
TransferStatusReceiver,
TxSnapshot,
} from "../interface";
import { NomicProviderId } from "./utils";

export class NomicTransferStatusProvider implements TransferStatusProvider {
readonly providerId = NomicProviderId;
readonly sourceDisplayName = "Nomic Bridge";
public statusReceiverDelegate?: TransferStatusReceiver;

constructor(
protected readonly chainList: Chain[],
readonly env: BridgeEnvironment
) {}

/** Request to start polling a new transaction. */
async trackTxStatus(snapshot: TxSnapshot): Promise<void> {
const { sendTxHash } = snapshot;

if (!snapshot.nomicCheckpointIndex) {
throw new Error("Nomic checkpoint index is required. Skipping tracking.");
}

await poll({
fn: async () => {
const checkpoint = await getCheckpoint(
{
relayers: getNomicRelayerUrl({ env: this.env }),
bitcoinNetwork: this.env === "mainnet" ? "bitcoin" : "testnet",
},
snapshot.nomicCheckpointIndex!
);

if (isNil(checkpoint.txid)) {
return;
}

return {
id: snapshot.sendTxHash,
status: "success",
} as BridgeTransferStatus;
},
validate: (incomingStatus) => incomingStatus !== undefined,
interval: 30_000,
maxAttempts: undefined, // unlimited attempts while tab is open or until success/fail
})
.then((s) => {
if (s) this.receiveConclusiveStatus(sendTxHash, s);
})
.catch((e) => console.error(`Polling Nomic has failed`, e));
}

receiveConclusiveStatus(
sendTxHash: string,
txStatus: BridgeTransferStatus | undefined
): void {
if (txStatus && txStatus.id) {
const { status, reason } = txStatus;
this.statusReceiverDelegate?.receiveNewTxStatus(
sendTxHash,
status,
reason
);
} else {
console.error(
"Nomic transfer finished poll but neither succeeded or failed"
);
}
}

makeExplorerUrl(snapshot: TxSnapshot): string {
const {
sendTxHash,
fromChain: { chainId: fromChainId },
} = snapshot;

const chain = this.chainList.find(
(chain) => chain.chain_id === fromChainId
);

if (!chain) throw new Error("Chain not found: " + fromChainId);
if (chain.explorers.length === 0) {
// attempt to link to mintscan since this is an IBC transfer
return `https://www.mintscan.io/${chain.chain_name}/txs/${sendTxHash}`;
}

return chain.explorers[0].tx_page.replace("{txHash}", sendTxHash);
}
}
1 change: 1 addition & 0 deletions packages/bridge/src/nomic/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const NomicProviderId = "Nomic";
65 changes: 65 additions & 0 deletions packages/bridge/src/penumbra/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
BridgeChain,
BridgeExternalUrl,
BridgeProvider,
BridgeProviderContext,
BridgeQuote,
BridgeSupportedAsset,
BridgeTransactionRequest,
GetBridgeSupportedAssetsParams,
} from "../interface";

export class PenumbraBridgeProvider implements BridgeProvider {
static readonly ID = "Penumbra";
readonly providerName = PenumbraBridgeProvider.ID;

constructor(protected readonly ctx: BridgeProviderContext) {}

async getQuote(): Promise<BridgeQuote> {
throw new Error("Penumbra quotes are currently not supported.");
}

async getSupportedAssets({
asset,
}: GetBridgeSupportedAssetsParams): Promise<
(BridgeChain & BridgeSupportedAsset)[]
> {
// just supports SOL via Penumbra

const assetListAsset = this.ctx.assetLists
.flatMap(({ assets }) => assets)
.find(
(a) => a.coinMinimalDenom.toLowerCase() === asset.address.toLowerCase()
);

if (assetListAsset) {
const penumbraCounterparty = assetListAsset.counterparty.find(
(c) => c.chainName === "penumbra"
);

if (penumbraCounterparty) {
return [
{
transferTypes: ["external-url"],
chainId: "penumbra",
chainName: "Penumbra",
chainType: "penumbra",
denom: penumbraCounterparty.symbol,
address: penumbraCounterparty.sourceDenom,
decimals: penumbraCounterparty.decimals,
},
];
}
}

return [];
}

async getTransactionData(): Promise<BridgeTransactionRequest> {
throw new Error("Penumbra transactions are currently not supported.");
}

async getExternalUrl(): Promise<BridgeExternalUrl | undefined> {
throw new Error("Penumbra external urls are currently not supported.");
}
}
Loading

0 comments on commit e5e3ff4

Please sign in to comment.