Skip to content
Open
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
8 changes: 8 additions & 0 deletions .changeset/tall-lizards-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@swapkit/wallet-extensions": patch
"@swapkit/wallet-hardware": patch
"@swapkit/wallets": patch
"@swapkit/sdk": patch
---

Enable KeepKey BEX and Vultisig THORChain/Maya direct swap submission by translating provided toolbox transactions into provider sign-and-broadcast requests. Enable Vultisig BTC, Cosmos, Kujira, and Ripple direct swap submission through intent extraction and mark Solana direct signing support. Stop advertising unsupported KeepKey BEX XRP/Solana and Vultisig Polkadot combinations. Add KeepKey SDK UTXO direct swap submission by signing the provided PSBT transaction and broadcasting the serialized transaction returned by KeepKey.
15 changes: 3 additions & 12 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,12 @@
"types": "./dist/types/wallets.d.ts"
}
},
"files": [
"dist/",
"src/"
],
"files": ["dist/", "src/"],
"homepage": "https://github.com/swapkit/wallets",
"license": "SEE LICENSE IN LICENSE",
"name": "@swapkit/sdk",
"publishConfig": {
"access": "public"
},
"repository": {
"directory": "packages/sdk",
"type": "git",
"url": "git+https://github.com/swapkit/wallets.git"
},
"publishConfig": { "access": "public" },
"repository": { "directory": "packages/sdk", "type": "git", "url": "git+https://github.com/swapkit/wallets.git" },
"scripts": {
"build": "bun run ./build.ts",
"build:clean": "rm -rf dist && bun run ./build.ts",
Expand Down
3 changes: 0 additions & 3 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { okxWallet } from "@swapkit/wallets/okx";
import { onekeyWallet } from "@swapkit/wallets/onekey";
import { passkeysWallet } from "@swapkit/wallets/passkeys";
import { phantomWallet } from "@swapkit/wallets/phantom";
import { radixWallet } from "@swapkit/wallets/radix";
import { talismanWallet } from "@swapkit/wallets/talisman";
import { trezorWallet } from "@swapkit/wallets/trezor";
import { tronlinkWallet } from "@swapkit/wallets/tronlink";
Expand Down Expand Up @@ -70,7 +69,6 @@ export {
onekeyWallet,
passkeysWallet,
phantomWallet,
radixWallet,
talismanWallet,
trezorWallet,
tronlinkWallet,
Expand Down Expand Up @@ -107,7 +105,6 @@ export const defaultWallets = {
...onekeyWallet,
...phantomWallet,
...passkeysWallet,
...radixWallet,
...talismanWallet,
...trezorWallet,
...tronlinkWallet,
Expand Down
9 changes: 2 additions & 7 deletions packages/wallet-extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,11 @@
"types": "./dist/types/vultisig/index.d.ts"
}
},
"files": [
"dist/",
"src/"
],
"files": ["dist/", "src/"],
"homepage": "https://github.com/swapkit/wallets",
"license": "SEE LICENSE IN LICENSE",
"name": "@swapkit/wallet-extensions",
"publishConfig": {
"access": "public"
},
"publishConfig": { "access": "public" },
"repository": {
"directory": "packages/wallet-extensions",
"type": "git",
Expand Down
128 changes: 128 additions & 0 deletions packages/wallet-extensions/src/helpers/tclikeTransferIntent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { type Chain, CosmosChainPrefixes, getChainConfig, SwapKitError } from "@swapkit/helpers";
import { base64ToBech32 } from "@swapkit/toolboxes/cosmos";

export type TCLikeChain = typeof Chain.THORChain | typeof Chain.Maya;

type TCLikeTransferMessage = {
typeUrl?: string;
type?: string;
value: {
amount: { amount: string; denom: string }[];
fromAddress?: string;
from_address?: string;
toAddress?: string;
to_address?: string;
};
};

type TCLikeDepositMessage = {
typeUrl?: string;
type?: string;
value: {
coins: { amount: string; asset: string | { chain?: string; symbol?: string; ticker?: string } }[];
memo?: string;
signer: string;
};
};

export type TCLikeToolboxTransaction = {
fee?: { gas?: string };
memo?: string;
msgs: Array<TCLikeTransferMessage | TCLikeDepositMessage>;
};

export type TCLikeTransferIntent = {
amount: { amount: number; decimals: number };
asset: { chain: string; symbol: string; ticker: string };
from: string;
gasLimit?: string;
memo: string;
method: "deposit" | "transfer";
recipient: string;
};

function getTCLikeTransactionMethod(tx: TCLikeToolboxTransaction): TCLikeTransferIntent["method"] {
const [msg] = tx.msgs;
if (!msg) throw new SwapKitError("plugin_swapkit_invalid_transaction");

const messageType = msg.typeUrl || msg.type;
if (messageType?.includes("MsgDeposit") || "coins" in msg.value) return "deposit";
if (messageType?.includes("MsgSend") || "amount" in msg.value) return "transfer";

throw new SwapKitError("plugin_swapkit_invalid_transaction", { messageType });
}

function normalizeTCLikeAddress(address: string, chain: TCLikeChain) {
const prefix = CosmosChainPrefixes[chain];
if (address.startsWith(`${prefix}1`)) return address;

return base64ToBech32(address, prefix);
}

function getAssetFromDepositCoin(asset: TCLikeDepositMessage["value"]["coins"][number]["asset"], chain: TCLikeChain) {
if (typeof asset === "string") {
const [assetChain = chain, symbol = assetChain] = asset.includes(".") ? asset.split(".") : [chain, asset];
return { chain: assetChain, symbol: symbol.toUpperCase(), ticker: symbol.split("-")[0]?.toUpperCase() || symbol };
}

const symbol = asset.symbol || asset.ticker || chain;
const ticker = asset.ticker || symbol.split("-")[0] || symbol;

return { chain: asset.chain || chain, symbol: symbol.toUpperCase(), ticker: ticker.toUpperCase() };
}

function getAssetFromTransferDenom(denom: string, chain: TCLikeChain) {
const symbol = (denom.includes(".") ? denom.split(".").at(-1) || denom : denom).toUpperCase();
const ticker = symbol.split("-")[0] || symbol;

return { chain, symbol, ticker };
}

export function extractTCLikeTransferIntent({
chain,
tx,
}: {
chain: TCLikeChain;
tx: TCLikeToolboxTransaction;
}): TCLikeTransferIntent {
const [msg] = tx.msgs;
if (!msg) throw new SwapKitError("plugin_swapkit_invalid_transaction");

const method = getTCLikeTransactionMethod(tx);
if (method === "deposit") {
const { coins, memo = tx.memo || "", signer } = (msg as TCLikeDepositMessage).value;
const [coin] = coins;
if (!coin) throw new SwapKitError("plugin_swapkit_invalid_transaction");

const asset = getAssetFromDepositCoin(coin.asset, chain);

return {
amount: { amount: Number(coin.amount), decimals: getChainConfig(chain).baseDecimal },
asset,
from: normalizeTCLikeAddress(signer, chain),
gasLimit: tx.fee?.gas,
memo,
method,
recipient: "",
};
}

const { amount, fromAddress, from_address, toAddress, to_address } = (msg as TCLikeTransferMessage).value;
const [coin] = amount;
const from = fromAddress || from_address;
const recipient = toAddress || to_address;

if (!(coin && from && recipient)) throw new SwapKitError("plugin_swapkit_invalid_transaction");

const asset = getAssetFromTransferDenom(coin.denom, chain);

return {
amount: { amount: Number(coin.amount), decimals: getChainConfig(chain).baseDecimal },
asset,
from: normalizeTCLikeAddress(from, chain),
gasLimit: tx.fee?.gas,
memo: tx.memo || "",
method,
recipient,
};
}
102 changes: 102 additions & 0 deletions packages/wallet-extensions/src/helpers/vultisigTransferIntent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Chain, getChainConfig, SwapKitError } from "@swapkit/helpers";
import type { CosmosTransaction } from "@swapkit/helpers/api";
import type { RippleTransaction } from "@swapkit/toolboxes/ripple";

export type VultisigNativeTransferIntent = {
amount: { amount: number; decimals: number };
asset: { chain: string; symbol: string; ticker: string };
from: string;
memo: string;
recipient: string;
};

type CosmosSendMessage = {
typeUrl?: string;
type?: string;
value: {
amount: { amount: string; denom: string }[];
fromAddress?: string;
from_address?: string;
toAddress?: string;
to_address?: string;
};
};

function getNativeCosmosSymbol(chain: Chain.Cosmos | Chain.Kujira) {
return chain === Chain.Kujira ? "KUJI" : "ATOM";
}

export function extractVultisigCosmosTransferIntent({
chain,
tx,
}: {
chain: Chain.Cosmos | Chain.Kujira;
tx: CosmosTransaction;
}): VultisigNativeTransferIntent {
const [msg] = tx.msgs as CosmosSendMessage[];
const messageType = msg?.typeUrl || msg?.type;

if (!(msg && messageType?.includes("MsgSend"))) {
throw new SwapKitError("plugin_swapkit_invalid_transaction", {
chain,
messageType,
reason: "Vultisig Cosmos/Kujira direct signing only supports native MsgSend transactions",
});
}

const [coin] = msg.value.amount;
const from = msg.value.fromAddress || msg.value.from_address;
const recipient = msg.value.toAddress || msg.value.to_address;

if (!(coin && from && recipient)) throw new SwapKitError("plugin_swapkit_invalid_transaction", { chain });

const symbol = getNativeCosmosSymbol(chain);

return {
amount: { amount: Number(coin.amount), decimals: getChainConfig(chain).baseDecimal },
asset: { chain, symbol, ticker: symbol },
from,
memo: tx.memo || "",
recipient,
};
}

function decodeXrplMemo(tx: RippleTransaction) {
const [memo] = "Memos" in tx && Array.isArray(tx.Memos) ? tx.Memos : [];
const memoData = memo?.Memo?.MemoData;
if (!memoData) return "";

try {
return Buffer.from(memoData, "hex").toString("utf8");
} catch {
return "";
}
}

export function extractVultisigRippleTransferIntent(tx: RippleTransaction): VultisigNativeTransferIntent {
if (tx.TransactionType !== "Payment") {
throw new SwapKitError("plugin_swapkit_invalid_transaction", {
chain: Chain.Ripple,
reason: "Vultisig Ripple direct signing only supports native Payment transactions",
transactionType: tx.TransactionType,
});
}

if (typeof tx.Amount !== "string") {
throw new SwapKitError("plugin_swapkit_invalid_transaction", {
chain: Chain.Ripple,
reason: "Vultisig Ripple direct signing only supports native XRP payments",
});
}

if (!(tx.Account && tx.Destination))
throw new SwapKitError("plugin_swapkit_invalid_transaction", { chain: Chain.Ripple });

return {
amount: { amount: Number(tx.Amount), decimals: getChainConfig(Chain.Ripple).baseDecimal },
asset: { chain: Chain.Ripple, symbol: Chain.Ripple, ticker: Chain.Ripple },
from: tx.Account,
memo: decodeXrplMemo(tx),
recipient: tx.Destination,
};
}
32 changes: 29 additions & 3 deletions packages/wallet-extensions/src/keepkey-bex/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { AssetValue, Chain, ChainId, filterSupportedChains, SwapKitError, WalletOption } from "@swapkit/helpers";
import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core";
import type { Eip1193Provider } from "ethers";
import { extractTCLikeTransferIntent } from "../helpers/tclikeTransferIntent";
import { extractUtxoTransferIntent, unsupportedUtxoSignTransaction } from "../helpers/utxoTransferIntent";
import type { ExtensionWallet } from "../walletTypes";
import {
getKEEPKEYAddress,
getKEEPKEYMethods,
getKEEPKEYProvider,
getProviderNameFromChain,
submitKeepkeyBexTransaction,
type WalletTxParams,
walletTransfer,
} from "./walletHelpers";
Expand Down Expand Up @@ -41,10 +43,11 @@ export const keepkeyBexWallet: ExtensionWallet<"connectKeepkeyBex"> = createWall
[Chain.Ethereum]: true,
[Chain.Kujira]: true,
[Chain.Litecoin]: true,
[Chain.Maya]: true,
[Chain.Optimism]: true,
[Chain.Polygon]: true,
[Chain.THORChain]: true,
[Chain.XLayer]: true,
// Ripple/Solana/THORChain/Maya: provider lacks raw-sign RPC
},
name: "connectKeepkeyBex",
supportedChains: [
Expand All @@ -63,8 +66,6 @@ export const keepkeyBexWallet: ExtensionWallet<"connectKeepkeyBex"> = createWall
Chain.Maya,
Chain.Optimism,
Chain.Polygon,
Chain.Ripple,
Chain.Solana,
Chain.THORChain,
Chain.XLayer,
],
Expand All @@ -85,6 +86,31 @@ async function getWalletMethods(chain: (typeof KEEPKEY_BEX_SUPPORTED_CHAINS)[num
return {
...toolbox,
deposit: (tx: WalletTxParams) => walletTransfer({ ...tx, recipient: "" }, "deposit"),
signAndBroadcastTransaction: (tx: Parameters<typeof extractTCLikeTransferIntent>[0]["tx"]) => {
const intent = extractTCLikeTransferIntent({ chain, tx });
return submitKeepkeyBexTransaction({
chain,
method: intent.method,
params: [
{
amount: intent.amount,
asset: intent.asset,
from: intent.from,
gasLimit: intent.gasLimit,
memo: intent.memo,
recipient: intent.recipient,
},
],
});
},
signTransaction: () =>
Promise.reject(
new SwapKitError("wallet_walletconnect_method_not_supported", {
method: "signTransaction",
reason: "KeepKey BEX THORChain/Maya provider only supports signAndBroadcastTransaction",
wallet: WalletOption.KEEPKEY_BEX,
}),
),
transfer: (tx: WalletTxParams) => walletTransfer({ ...tx, gasLimit }, "transfer"),
};
}
Expand Down
Loading