diff --git a/packages/bridge/src/bridge-providers.ts b/packages/bridge/src/bridge-providers.ts
index e3bd7d95aa..1d2c81a5e6 100644
--- a/packages/bridge/src/bridge-providers.ts
+++ b/packages/bridge/src/bridge-providers.ts
@@ -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";
@@ -21,6 +22,7 @@ export class BridgeProviders {
[WormholeBridgeProvider.ID]: WormholeBridgeProvider;
[NitroBridgeProvider.ID]: NitroBridgeProvider;
[PicassoBridgeProvider.ID]: PicassoBridgeProvider;
+ [PenumbraBridgeProvider.ID]: PenumbraBridgeProvider;
};
constructor(integratorId: string, commonContext: BridgeProviderContext) {
@@ -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),
};
}
}
diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts
index 24c4929ce3..33a8395980 100644
--- a/packages/bridge/src/interface.ts
+++ b/packages/bridge/src/interface.ts
@@ -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;
@@ -510,6 +517,7 @@ const txSnapshotSchema = z.object({
})
),
estimatedArrivalUnix: z.number(),
+ nomicCheckpointIndex: z.number().optional(),
});
export type TxSnapshot = z.infer;
diff --git a/packages/bridge/src/nomic/index.ts b/packages/bridge/src/nomic/index.ts
index cfdab75f4b..a7928d7c67 100644
--- a/packages/bridge/src/nomic/index.ts
+++ b/packages/bridge/src/nomic/index.ts
@@ -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";
@@ -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[];
@@ -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 {
@@ -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];
@@ -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,
diff --git a/packages/bridge/src/nomic/transfer-status.ts b/packages/bridge/src/nomic/transfer-status.ts
new file mode 100644
index 0000000000..250b7e75ab
--- /dev/null
+++ b/packages/bridge/src/nomic/transfer-status.ts
@@ -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 {
+ 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);
+ }
+}
diff --git a/packages/bridge/src/nomic/utils.ts b/packages/bridge/src/nomic/utils.ts
new file mode 100644
index 0000000000..7f2908ffda
--- /dev/null
+++ b/packages/bridge/src/nomic/utils.ts
@@ -0,0 +1 @@
+export const NomicProviderId = "Nomic";
diff --git a/packages/bridge/src/penumbra/index.ts b/packages/bridge/src/penumbra/index.ts
new file mode 100644
index 0000000000..4ad5078da8
--- /dev/null
+++ b/packages/bridge/src/penumbra/index.ts
@@ -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 {
+ 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 {
+ throw new Error("Penumbra transactions are currently not supported.");
+ }
+
+ async getExternalUrl(): Promise {
+ throw new Error("Penumbra external urls are currently not supported.");
+ }
+}
diff --git a/packages/stores/src/account/base.ts b/packages/stores/src/account/base.ts
index b09fc7b4ac..48ac2c410f 100644
--- a/packages/stores/src/account/base.ts
+++ b/packages/stores/src/account/base.ts
@@ -216,7 +216,7 @@ export class AccountStore[] = []> {
makeObservable(this);
autorun(async () => {
- const isOneClickTradingEnabled = await this.getShouldUseOneClickTrading();
+ const isOneClickTradingEnabled = await this.isOneClickTradingEnabled();
const oneClickTradingInfo = await this.getOneClickTradingInfo();
const hasUsedOneClickTrading = await this.getHasUsedOneClickTrading();
runInAction(() => {
@@ -515,11 +515,12 @@ export class AccountStore[] = []> {
fee?: StdFee,
signOptions?: SignOptions,
onTxEvents?:
- | ((tx: DeliverTxResponse) => void)
+ | ((tx: DeliverTxResponse) => void | Promise)
| {
onBroadcastFailed?: (e?: Error) => void;
onBroadcasted?: (txHash: Uint8Array) => void;
onFulfill?: (tx: DeliverTxResponse) => void;
+ onSign?: () => Promise | void;
}
) {
runInAction(() => {
@@ -547,6 +548,7 @@ export class AccountStore[] = []> {
let onBroadcasted: ((txHash: Uint8Array) => void) | undefined;
let onFulfill: ((tx: DeliverTxResponse) => void) | undefined;
+ let onSign: (() => Promise | void) | undefined;
if (onTxEvents) {
if (typeof onTxEvents === "function") {
@@ -554,6 +556,7 @@ export class AccountStore[] = []> {
} else {
onBroadcasted = onTxEvents?.onBroadcasted;
onFulfill = onTxEvents?.onFulfill;
+ onSign = onTxEvents?.onSign;
}
}
@@ -598,6 +601,14 @@ export class AccountStore[] = []> {
const { TxRaw } = await import("cosmjs-types/cosmos/tx/v1beta1/tx");
const encodedTx = TxRaw.encode(txRaw).finish();
+ if (this.options.preTxEvents?.onSign) {
+ await this.options.preTxEvents.onSign();
+ }
+
+ if (onSign) {
+ await onSign();
+ }
+
const restEndpoint = getEndpointString(
await wallet.getRestEndpoint(true)
);
@@ -709,7 +720,7 @@ export class AccountStore[] = []> {
}
if (onFulfill) {
- onFulfill(tx);
+ await onFulfill(tx);
}
} catch (e) {
const error = e as Error | AccountStoreNoBroadcastErrorEvent;
@@ -1472,7 +1483,7 @@ export class AccountStore[] = []> {
}: {
messages: readonly EncodeObject[];
}): Promise {
- const isOneClickTradingEnabled = await this.isOneCLickTradingEnabled();
+ const isOneClickTradingEnabled = await this.isOneClickTradingEnabled();
const oneClickTradingInfo = await this.getOneClickTradingInfo();
if (!oneClickTradingInfo || !isOneClickTradingEnabled) {
@@ -1535,7 +1546,7 @@ export class AccountStore[] = []> {
});
}
- async isOneCLickTradingEnabled(): Promise {
+ async isOneClickTradingEnabled(): Promise {
const oneClickTradingInfo = await this.getOneClickTradingInfo();
if (isNil(oneClickTradingInfo)) return false;
diff --git a/packages/stores/src/account/osmosis/index.ts b/packages/stores/src/account/osmosis/index.ts
index c83a0c7784..31afc81845 100644
--- a/packages/stores/src/account/osmosis/index.ts
+++ b/packages/stores/src/account/osmosis/index.ts
@@ -10,7 +10,6 @@ import {
import * as OsmosisMath from "@osmosis-labs/math";
import { maxTick, minTick } from "@osmosis-labs/math";
import {
- makeAddAuthenticatorMsg,
makeAddToConcentratedLiquiditySuperfluidPositionMsg,
makeAddToPositionMsg,
makeBeginUnlockingMsg,
@@ -27,7 +26,6 @@ import {
makeJoinSwapExternAmountInMsg,
makeLockAndSuperfluidDelegateMsg,
makeLockTokensMsg,
- makeRemoveAuthenticatorMsg,
makeSetValidatorSetPreferenceMsg,
makeSplitRoutesSwapExactAmountInMsg,
makeSplitRoutesSwapExactAmountOutMsg,
@@ -2203,154 +2201,6 @@ export class OsmosisAccountImpl {
);
}
- async sendAddOrRemoveAuthenticatorsMsg({
- addAuthenticators,
- removeAuthenticators,
- memo = "",
- onFulfill,
- onBroadcasted,
- signOptions,
- }: {
- addAuthenticators: { authenticatorType: string; data: Uint8Array }[];
- removeAuthenticators: bigint[];
- memo?: string;
- onFulfill?: (tx: DeliverTxResponse) => void;
- onBroadcasted?: () => void;
- signOptions?: SignOptions;
- }) {
- const addAuthenticatorMsgs = addAuthenticators.map((authenticator) =>
- makeAddAuthenticatorMsg({
- authenticatorType: authenticator.authenticatorType,
- data: authenticator.data,
- sender: this.address,
- })
- );
- const removeAuthenticatorMsgs = removeAuthenticators.map((id) =>
- makeRemoveAuthenticatorMsg({
- id,
- sender: this.address,
- })
- );
- const msgs = await Promise.all([
- ...removeAuthenticatorMsgs,
- ...addAuthenticatorMsgs,
- ]);
-
- await this.base.signAndBroadcast(
- this.chainId,
- "addOrRemoveAuthenticators",
- msgs,
- memo,
- undefined,
- signOptions,
- {
- onBroadcasted,
- onFulfill: (tx) => {
- if (!tx.code) {
- // Refresh the balances
- const queries = this.queriesStore.get(this.chainId);
-
- queries.queryBalances
- .getQueryBech32Address(this.address)
- .balances.forEach((balance) => balance.waitFreshResponse());
-
- queries.cosmos.queryDelegations
- .getQueryBech32Address(this.address)
- .waitFreshResponse();
-
- queries.cosmos.queryRewards
- .getQueryBech32Address(this.address)
- .waitFreshResponse();
- }
- onFulfill?.(tx);
- },
- }
- );
- }
-
- async sendAddAuthenticatorsMsg(
- authenticators: { authenticatorType: string; data: any }[],
- memo: string = "",
- onFulfill?: (tx: DeliverTxResponse) => void
- ) {
- const addAuthenticatorMsgs = await Promise.all(
- authenticators.map((authenticator) =>
- makeAddAuthenticatorMsg({
- authenticatorType: authenticator.authenticatorType,
- data: authenticator.data,
- sender: this.address,
- })
- )
- );
-
- await this.base.signAndBroadcast(
- this.chainId,
- "addAuthenticator",
- addAuthenticatorMsgs,
- memo,
- undefined,
- undefined,
- (tx) => {
- if (!tx.code) {
- // Refresh the balances
- const queries = this.queriesStore.get(this.chainId);
-
- queries.queryBalances
- .getQueryBech32Address(this.address)
- .balances.forEach((balance) => balance.waitFreshResponse());
-
- queries.cosmos.queryDelegations
- .getQueryBech32Address(this.address)
- .waitFreshResponse();
-
- queries.cosmos.queryRewards
- .getQueryBech32Address(this.address)
- .waitFreshResponse();
- }
- onFulfill?.(tx);
- }
- );
- }
-
- async sendRemoveAuthenticatorMsg(
- id: bigint,
- memo: string = "",
- onFulfill?: (tx: DeliverTxResponse) => void
- ) {
- const removeAuthenticatorMsg = await makeRemoveAuthenticatorMsg({
- id: id,
- sender: this.address,
- });
-
- await this.base.signAndBroadcast(
- this.chainId,
- "removeAuthenticator",
- [removeAuthenticatorMsg],
- memo,
- undefined,
- undefined,
- (tx) => {
- if (!tx.code) {
- // Refresh the balances
- const queries = this.queriesStore.get(this.chainId);
-
- queries.queryBalances
- .getQueryBech32Address(this.address)
- .balances.forEach((balance) => balance.waitFreshResponse());
-
- queries.cosmos.queryDelegations
- .getQueryBech32Address(this.address)
- .waitFreshResponse();
-
- queries.cosmos.queryRewards
- .getQueryBech32Address(this.address)
- .waitFreshResponse();
- }
- onFulfill?.(tx);
- }
- );
- }
-
protected get queries() {
return this.queriesStore.get(this.chainId).osmosis!;
}
diff --git a/packages/stores/src/account/types.ts b/packages/stores/src/account/types.ts
index fe8ea8faee..2160735f11 100644
--- a/packages/stores/src/account/types.ts
+++ b/packages/stores/src/account/types.ts
@@ -87,9 +87,10 @@ export type AccountStoreWallet[] = []> =
};
export interface TxEvents {
- onBroadcastFailed?: (string: string, e?: Error) => void;
- onBroadcasted?: (string: string, txHash: Uint8Array) => void;
- onFulfill?: (string: string, tx: any) => void;
+ onSign?: () => Promise | void;
+ onBroadcastFailed?: (chainNameOrId: string, e?: Error) => void;
+ onBroadcasted?: (chainNameOrId: string, txHash: Uint8Array) => void;
+ onFulfill?: (chainNameOrId: string, tx: any) => void;
onExceeds1CTNetworkFeeLimit?: (params: {
// Continue with a wallet like Keplr.
continueTx: () => void;
diff --git a/packages/utils/src/__tests__/string.spec.ts b/packages/utils/src/__tests__/string.spec.ts
index 033b0a1f46..a4e2d208ef 100644
--- a/packages/utils/src/__tests__/string.spec.ts
+++ b/packages/utils/src/__tests__/string.spec.ts
@@ -61,39 +61,65 @@ describe("shorten", () => {
describe("isBitcoinAddressValid", () => {
it("should return true for a valid Bitcoin address (P2PKH)", () => {
const validAddress = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
- expect(isBitcoinAddressValid({ address: validAddress })).toBe(true);
+ expect(
+ isBitcoinAddressValid({ address: validAddress, env: "mainnet" })
+ ).toBe(true);
});
it("should return true for a valid Bitcoin address (P2SH)", () => {
const validAddress = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy";
- expect(isBitcoinAddressValid({ address: validAddress })).toBe(true);
+ expect(
+ isBitcoinAddressValid({ address: validAddress, env: "mainnet" })
+ ).toBe(true);
});
it("should return true for a valid Bitcoin address (P2WPKH)", () => {
const validAddress = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
- expect(isBitcoinAddressValid({ address: validAddress })).toBe(true);
+ expect(
+ isBitcoinAddressValid({ address: validAddress, env: "mainnet" })
+ ).toBe(true);
});
it("should return true for a valid testnet Bitcoin address ", () => {
const validAddress = "tb1qq9epaj33z79vwz5zu9gw40j00yma7cm7g2ympl";
expect(
- isBitcoinAddressValid({ address: validAddress, isTestnet: true })
+ isBitcoinAddressValid({ address: validAddress, env: "testnet" })
).toBe(true);
});
it("should return false for an invalid Bitcoin address", () => {
const invalidAddress = "invalidBitcoinAddress";
- expect(isBitcoinAddressValid({ address: invalidAddress })).toBe(false);
+ expect(
+ isBitcoinAddressValid({ address: invalidAddress, env: "mainnet" })
+ ).toBe(false);
});
it("should return false for an empty address", () => {
const emptyAddress = "";
- expect(isBitcoinAddressValid({ address: emptyAddress })).toBe(false);
+ expect(
+ isBitcoinAddressValid({ address: emptyAddress, env: "mainnet" })
+ ).toBe(false);
});
it("should return false for a malformed address", () => {
const malformedAddress = "12345";
- expect(isBitcoinAddressValid({ address: malformedAddress })).toBe(false);
+ expect(
+ isBitcoinAddressValid({ address: malformedAddress, env: "mainnet" })
+ ).toBe(false);
+ });
+
+ it("should return false for a testnet address in mainnet", () => {
+ const testnetAddress = "tb1qq9epaj33z79vwz5zu9gw40j00yma7cm7g2ympl";
+ expect(
+ isBitcoinAddressValid({ address: testnetAddress, env: "mainnet" })
+ ).toBe(false);
+ });
+
+ it("should return true for a mainnet address in mainnet", () => {
+ const mainnetAddress = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
+ expect(
+ isBitcoinAddressValid({ address: mainnetAddress, env: "mainnet" })
+ ).toBe(true);
});
});
diff --git a/packages/utils/src/bitcoin.ts b/packages/utils/src/bitcoin.ts
index b72303af94..68035ab0eb 100644
--- a/packages/utils/src/bitcoin.ts
+++ b/packages/utils/src/bitcoin.ts
@@ -3,6 +3,7 @@ export const BitcoinChainInfo = {
chainId: "bitcoin",
chainName: "Bitcoin",
color: "#F7931A",
+ logoUri: "/networks/bitcoin.svg",
};
export const BitcoinMainnetExplorerUrl =
@@ -39,5 +40,11 @@ export const getnBTCMinimalDenom = ({
}) => {
return env === "mainnet"
? "ibc/75345531D87BD90BF108BE7240BD721CB2CB0A1F16D4EBA71B09EC3C43E15C8F" // nBTC
- : "ibc/72D483F0FD4229DBF3ACC78E648F0399C4ACADDFDBCDD9FE791FEE4443343422"; // Testnet nBTC
+ : "ibc/8D294CE85345F171AAF6B1FF6E64B5A9EE197C99CDAD64D79EA4ACAB270AC95C"; // Testnet nBTC
};
+
+export function getNomicRelayerUrl({ env }: { env: "mainnet" | "testnet" }) {
+ return env === "testnet"
+ ? ["https://testnet-relayer.nomic.io:8443"]
+ : ["https://relayer.nomic.mappum.io:8443"];
+}
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 07493feca3..ca00506a85 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -15,6 +15,7 @@ export * from "./gas-utils";
export * from "./ibc-utils";
export * from "./math";
export * from "./object";
+export * from "./penumbra";
export * from "./poll";
export * from "./solana";
export * from "./sort";
diff --git a/packages/utils/src/penumbra.ts b/packages/utils/src/penumbra.ts
new file mode 100644
index 0000000000..0d19c06f42
--- /dev/null
+++ b/packages/utils/src/penumbra.ts
@@ -0,0 +1,6 @@
+export const PenumbraChainInfo = {
+ prettyName: "Penumbra",
+ chainId: "penumbra",
+ chainName: "Penumbra",
+ color: "#ff902f",
+};
diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts
index ac7acdceb1..8c208f17e0 100644
--- a/packages/utils/src/string.ts
+++ b/packages/utils/src/string.ts
@@ -76,19 +76,42 @@ export function isEvmAddressValid({ address }: { address: string }): boolean {
export function isBitcoinAddressValid({
address,
- isTestnet = false,
+ env,
}: {
address: string;
- isTestnet?: boolean;
+ env: "mainnet" | "testnet";
}): boolean {
try {
- bitcoin.address.toOutputScript(
- address,
- isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin
- );
- return true;
+ // Attempt to decode the address
+ const decoded = bitcoin.address.fromBase58Check(address);
+ const isTestnet =
+ decoded.version === bitcoin.networks.testnet.pubKeyHash ||
+ decoded.version === bitcoin.networks.testnet.scriptHash;
+ const isMainnet =
+ decoded.version === bitcoin.networks.bitcoin.pubKeyHash ||
+ decoded.version === bitcoin.networks.bitcoin.scriptHash;
+
+ if ((env === "mainnet" && isMainnet) || (env === "testnet" && isTestnet)) {
+ return true; // Address is valid for the given environment
+ }
+ return false; // Address is invalid for the given environment
} catch (e) {
- return false;
+ try {
+ // If Base58 decoding fails, try Bech32 decoding
+ const decoded = bitcoin.address.fromBech32(address);
+ const isTestnet = decoded.prefix === "tb" || decoded.prefix === "bcrt";
+ const isMainnet = decoded.prefix === "bc";
+
+ if (
+ (env === "mainnet" && isMainnet) ||
+ (env === "testnet" && isTestnet)
+ ) {
+ return true; // Address is valid for the given environment
+ }
+ return false; // Address is invalid for the given environment
+ } catch (e) {
+ return false; // Address is invalid
+ }
}
}
diff --git a/packages/web/.env b/packages/web/.env
index d0f78286ff..cf30c16291 100644
--- a/packages/web/.env
+++ b/packages/web/.env
@@ -48,4 +48,7 @@ TWITTER_API_URL=https://api.twitter.com/
BLOCKAID_BASE_URL=http://api.blockaid.io:80
# BLOCKAID_API_KEY=
-NEXT_PUBLIC_SPEND_LIMIT_CONTRACT_ADDRESS=osmo10xqv8rlpkflywm92k5wdmplzy7khtasl9c2c08psmvlu543k724sy94k74
\ No newline at end of file
+NEXT_PUBLIC_SPEND_LIMIT_CONTRACT_ADDRESS=osmo10xqv8rlpkflywm92k5wdmplzy7khtasl9c2c08psmvlu543k724sy94k74
+
+# Disable TRPC logs in development
+# NEXT_PUBLIC_TRPC_LOGS=off
diff --git a/packages/web/components/alert/__tests__/prettify.spec.ts b/packages/web/components/alert/__tests__/prettify.spec.ts
index 68a35e5f09..90e0bb343c 100644
--- a/packages/web/components/alert/__tests__/prettify.spec.ts
+++ b/packages/web/components/alert/__tests__/prettify.spec.ts
@@ -1,6 +1,11 @@
+import { Dec } from "@keplr-wallet/unit";
import cases from "jest-in-case";
-import { isOverspendErrorMessage, isRejectedTxErrorMessage } from "../prettify";
+import {
+ getParametersFromOverspendErrorMessage,
+ isOverspendErrorMessage,
+ isRejectedTxErrorMessage,
+} from "../prettify";
cases(
"isOverspendErrorMessage",
@@ -45,6 +50,53 @@ cases(
]
);
+cases(
+ "getParametersFromOverspendErrorMessage",
+ ({ message, result }) => {
+ expect(getParametersFromOverspendErrorMessage(message)).toEqual(result);
+ },
+ [
+ {
+ name: "should extract parameters from valid overspend error message",
+ message:
+ "Fetch error. Spend limit error: Overspend: 2000000 has been spent but limit is 1000000.",
+ result: {
+ wouldSpendTotal: new Dec("2000000", 6),
+ limit: new Dec("1000000", 6),
+ },
+ },
+ {
+ name: "should extract parameters from complex overspend error message",
+ message:
+ "Fetch error. execution blocked by authenticator (account = osmo1sh8lreekwcytxpqr6lxmw5cl7kdrfsdfat2ujlvz, authenticator id = 208, msg index = 0, msg type url = /osmosis.poolmanager.v1beta1.MsgSwapExactAmountIn): Spend limit error: Overspend: 50065777 has been spent but limit is 1000000: execute wasm contract failed",
+ result: {
+ wouldSpendTotal: new Dec("50065777", 6),
+ limit: new Dec("1000000", 6),
+ },
+ },
+ {
+ name: "should handle empty message",
+ message: "",
+ result: undefined,
+ },
+ {
+ name: "should handle undefined message",
+ message: undefined,
+ result: undefined,
+ },
+ {
+ name: "should return undefined for non-overspend error message",
+ message: "execution succeeded",
+ result: undefined,
+ },
+ {
+ name: "should return undefined for invalid overspend error format",
+ message: "Spend limit error: Invalid format",
+ result: undefined,
+ },
+ ]
+);
+
cases(
"isRejectedTxErrorMessage",
({ message, result }) => {
diff --git a/packages/web/components/alert/prettify.ts b/packages/web/components/alert/prettify.ts
index 7b53c3ea30..7f4038cdfd 100644
--- a/packages/web/components/alert/prettify.ts
+++ b/packages/web/components/alert/prettify.ts
@@ -1,5 +1,5 @@
import { AppCurrency } from "@keplr-wallet/types";
-import { CoinPretty, Int } from "@keplr-wallet/unit";
+import { CoinPretty, Dec, Int } from "@keplr-wallet/unit";
import {
isInsufficientFeeError,
isSlippageErrorMessage,
@@ -27,6 +27,34 @@ const regexRejectedTx = /Request rejected/;
const regexOverspendError =
/Spend limit error: Overspend: (\d+) has been spent but limit is (\d+)/;
+export function getParametersFromOverspendErrorMessage(
+ message: string | undefined
+): { wouldSpendTotal: Dec; limit: Dec } | undefined {
+ if (!message) return;
+
+ const match = message.match(regexOverspendError);
+ if (!match) return;
+
+ const [, wouldSpendTotal, limit] = match;
+
+ if (!wouldSpendTotal || !limit) return;
+
+ try {
+ // Validate that extracted values are valid numbers
+ if (isNaN(Number(wouldSpendTotal)) || isNaN(Number(limit))) {
+ return;
+ }
+
+ return {
+ wouldSpendTotal: new Dec(wouldSpendTotal, 6),
+ limit: new Dec(limit, 6),
+ };
+ } catch (error) {
+ console.error("Failed to parse overspend error parameters:", error);
+ return;
+ }
+}
+
export function isOverspendErrorMessage({
message,
}: {
diff --git a/packages/web/components/bridge/amount-screen.tsx b/packages/web/components/bridge/amount-screen.tsx
index 783aef9911..a620ad2e5c 100644
--- a/packages/web/components/bridge/amount-screen.tsx
+++ b/packages/web/components/bridge/amount-screen.tsx
@@ -424,6 +424,14 @@ export const AmountScreen = observer(
type: fromChain.chainType,
assets: assets as Extract[],
};
+ case "penumbra":
+ return {
+ type: fromChain.chainType,
+ assets: assets as Extract<
+ SupportedAsset,
+ { chainType: "penumbra" }
+ >[],
+ };
default:
return {
type: fromChain.chainType,
@@ -783,8 +791,6 @@ export const AmountScreen = observer(
/>
);
- console.log(fromAsset);
-
if (
featureFlags.bridgeDepositAddress &&
!quote.enabled &&
diff --git a/packages/web/components/bridge/bridge-wallet-select-modal.tsx b/packages/web/components/bridge/bridge-wallet-select-modal.tsx
index 1db0cc8f8f..1067b7de50 100644
--- a/packages/web/components/bridge/bridge-wallet-select-modal.tsx
+++ b/packages/web/components/bridge/bridge-wallet-select-modal.tsx
@@ -532,7 +532,7 @@ const SendToAnotherAddressForm: FunctionComponent<
} else if (toChain.chainType === "bitcoin") {
isValid = isBitcoinAddressValid({
address: nextValue,
- isTestnet: IS_TESTNET,
+ env: IS_TESTNET ? "testnet" : "mainnet",
});
}
@@ -570,7 +570,9 @@ const SendToAnotherAddressForm: FunctionComponent<
className="body1 cursor-pointer select-none text-osmoverse-300"
onClick={() => setIsAcknowledged(!isAcknowledged)}
>
- {t("transfer.acknowledgement")}
+ {toChain.chainType === "bitcoin"
+ ? t("transfer.acknowledgementWithoutExchange")
+ : t("transfer.acknowledgement")}