Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5fa4d0a
fix: add caching to Phantom wallet to prevent rate limiting
gomesalexandre Dec 15, 2025
62e99f0
chore: version packages to 1.62.29-phantom-cache.0
gomesalexandre Dec 15, 2025
614aab5
chore: update package.json files after publish attempt
gomesalexandre Dec 15, 2025
b02f6a4
chore: version packages to 1.62.29-phantom-cache.1
gomesalexandre Dec 15, 2025
4ea85c9
feat: enable Polygon, Monad, and HyperEVM support for Phantom wallet
gomesalexandre Dec 15, 2025
96df786
chore: version packages to 1.62.29-phantom-cache.2
gomesalexandre Dec 15, 2025
3e7cd05
chore: remove comments from Phantom support flags
gomesalexandre Dec 15, 2025
ac05e44
feat: add Sui support to Phantom wallet
gomesalexandre Dec 15, 2025
ef8fe9b
chore: version packages to 1.62.29-phantom-cache.3
gomesalexandre Dec 15, 2025
0526d34
feat: add EVM chain switching support to Phantom wallet
gomesalexandre Dec 15, 2025
d47ef60
chore: version packages to 1.62.29-phantom-cache.4
gomesalexandre Dec 15, 2025
9e50a52
chore: version packages to 1.62.29-phantom-cache.5
gomesalexandre Dec 15, 2025
f380d39
chore: version packages to 1.62.29-phantom-cache.6
gomesalexandre Dec 15, 2025
2575d5d
chore: version packages to 1.62.29-phantom-cache.7
gomesalexandre Dec 15, 2025
997cad8
fix: update Sui implementation to use requestAccount API
gomesalexandre Dec 15, 2025
d5301e6
chore: version packages to 1.62.29-phantom-cache.8
gomesalexandre Dec 15, 2025
0a29aff
chore: version packages to 1.62.29-phantom-cache.9
gomesalexandre Dec 15, 2025
7b9d940
chore: version packages to 1.62.29-phantom-cache.10
gomesalexandre Dec 15, 2025
828f39b
chore: version packages to 1.62.29-phantom-cache.11
gomesalexandre Dec 15, 2025
f17fe9e
chore: version packages to 1.62.29-phantom-cache.12
gomesalexandre Dec 15, 2025
7825741
chore: version packages to 1.62.29-phantom-cache.13
gomesalexandre Dec 15, 2025
eac8e34
chore: version packages to 1.62.29-phantom-cache.14
gomesalexandre Dec 15, 2025
72e42d5
chore: version packages to 1.62.29-phantom-sui-fix.0
gomesalexandre Dec 15, 2025
e1bd42f
fix: convert Phantom base64 signature to hex for chain adapter
gomesalexandre Dec 15, 2025
e504288
fix: extract signature parts from Phantom's combined signature to mat…
gomesalexandre Dec 15, 2025
303463c
chore: clean up Phantom Sui implementation
gomesalexandre Dec 15, 2025
5ebee5b
chore: apply lint fixes
gomesalexandre Dec 15, 2025
bdccc68
refactor: code cleanup for Phantom Sui implementation
gomesalexandre Dec 15, 2025
6907f61
fix: correct Phantom Sui provider type definition
gomesalexandre Dec 15, 2025
2761527
refactor: simplify code style and improve type definitions
gomesalexandre Dec 15, 2025
18a2786
revert: restore all package.json, lerna.json, and yarn.lock to origin…
gomesalexandre Dec 15, 2025
c618ae7
feat: trigger ci
gomesalexandre Dec 15, 2025
be3fb47
fix: add required transactionJson property to sui test
gomesalexandre Dec 15, 2025
3d986e3
Merge remote-tracking branch 'origin/master' into fix_phantom
gomesalexandre Dec 16, 2025
aca13b5
chore(release): publish 1.62.30
gomesalexandre Dec 16, 2025
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: 2 additions & 0 deletions packages/hdwallet-core/src/sui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface SuiSignTx {
addressNList: BIP32Path;
/** Intent message bytes to sign (intent scope + version + app id + tx bytes) */
intentMessageBytes: Uint8Array;
/** Transaction JSON string for wallet signing */
transactionJson: string;
}

export interface SuiSignedTx {
Expand Down
1 change: 1 addition & 0 deletions packages/hdwallet-native/src/__tests__/sui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("NativeHDWallet SUI", () => {
const signedTx = await wallet.suiSignTx({
addressNList: core.suiGetAccountPaths({ accountIdx: 0 })[0].addressNList,
intentMessageBytes: mockIntentMessage,
transactionJson: '{"mock": "transaction"}',
});

expect(signedTx).toBeTruthy();
Expand Down
6 changes: 4 additions & 2 deletions packages/hdwallet-phantom/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as core from "@shapeshiftoss/hdwallet-core";

import { PhantomHDWallet } from "./phantom";
import { PhantomEvmProvider, PhantomSolanaProvider, PhantomUtxoProvider } from "./types";
import { PhantomEvmProvider, PhantomSolanaProvider, PhantomSuiProvider, PhantomUtxoProvider } from "./types";

declare global {
interface Window {
phantom?: {
ethereum?: PhantomEvmProvider;
bitcoin?: PhantomUtxoProvider;
solana?: PhantomSolanaProvider;
sui?: PhantomSuiProvider;
};
}
}
Expand All @@ -32,6 +33,7 @@ export class PhantomAdapter {
const evmProvider = window.phantom?.ethereum;
const bitcoinProvider = window.phantom?.bitcoin;
const solanaProvider = window.phantom?.solana;
const suiProvider = window.phantom?.sui;

if (!evmProvider || !bitcoinProvider || !solanaProvider) {
window.open("https://phantom.app/", "_blank");
Expand All @@ -43,7 +45,7 @@ export class PhantomAdapter {
// Note, we don't try and get EVM accounts below just yet - we only use the Solana account as a deviceID
await solanaProvider.connect();

const wallet = new PhantomHDWallet(evmProvider, bitcoinProvider, solanaProvider);
const wallet = new PhantomHDWallet(evmProvider, bitcoinProvider, solanaProvider, suiProvider);
await wallet.initialize();
const deviceID = await wallet.getDeviceID();
this.keyring.add(wallet, deviceID);
Expand Down
1 change: 1 addition & 0 deletions packages/hdwallet-phantom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./adapter";
export * from "./phantom";
export * from "./sui";
105 changes: 87 additions & 18 deletions packages/hdwallet-phantom/src/phantom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import isObject from "lodash/isObject";
import * as btc from "./bitcoin";
import * as eth from "./ethereum";
import { solanaSendTx, solanaSignTx } from "./solana";
import { PhantomEvmProvider, PhantomSolanaProvider, PhantomUtxoProvider } from "./types";
import * as sui from "./sui";
import { PhantomEvmProvider, PhantomSolanaProvider, PhantomSuiProvider, PhantomUtxoProvider } from "./types";

export function isPhantom(wallet: core.HDWallet): wallet is PhantomHDWallet {
return isObject(wallet) && (wallet as any)._isPhantom;
}

export class PhantomHDWalletInfo
implements core.HDWalletInfo, core.BTCWalletInfo, core.ETHWalletInfo, core.SolanaWalletInfo
implements core.HDWalletInfo, core.BTCWalletInfo, core.ETHWalletInfo, core.SolanaWalletInfo, core.SuiWalletInfo
{
readonly _supportsBTCInfo = true;
readonly _supportsETHInfo = true;
readonly _supportsSolanaInfo = true;
readonly _supportsSuiInfo = true;

evmProvider: PhantomEvmProvider;

Expand Down Expand Up @@ -169,47 +171,63 @@ export class PhantomHDWalletInfo
public solanaNextAccountPath(msg: core.SolanaAccountPath): core.SolanaAccountPath | undefined {
throw new Error("Method not implemented");
}

/** Sui */

public suiGetAccountPaths(msg: core.SuiGetAccountPaths): Array<core.SuiAccountPath> {
return core.suiGetAccountPaths(msg);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public suiNextAccountPath(msg: core.SuiAccountPath): core.SuiAccountPath | undefined {
return undefined;
}
}

export class PhantomHDWallet
extends PhantomHDWalletInfo
implements core.HDWallet, core.BTCWallet, core.ETHWallet, core.SolanaWallet
implements core.HDWallet, core.BTCWallet, core.ETHWallet, core.SolanaWallet, core.SuiWallet
{
readonly _supportsBTC = true;
readonly _supportsETH = true;
readonly _supportsEthSwitchChain = false;
readonly _supportsEthSwitchChain = true;
readonly _supportsAvalanche = false;
readonly _supportsOptimism = false;
// Polygon is technically supported but is acting up on the Phantom side of things atm
// https://github.com/orgs/phantom/discussions/294
Comment thread
gomesalexandre marked this conversation as resolved.
readonly _supportsPolygon = false;
readonly _supportsPolygon = true;
readonly _supportsGnosis = false;
readonly _supportsArbitrum = false;
readonly _supportsArbitrumNova = false;
readonly _supportsBase = true;
readonly _supportsMonad = false;
readonly _supportsMonad = true;
readonly _supportsPlasma = false;
readonly _supportsHyperEvm = false;
readonly _supportsHyperEvm = true;
readonly _supportsBSC = false;
readonly _supportsSolana = true;
readonly _supportsSui = true;
readonly _isPhantom = true;

evmProvider: PhantomEvmProvider;
bitcoinProvider: PhantomUtxoProvider;
solanaProvider: PhantomSolanaProvider;
suiProvider?: PhantomSuiProvider;

ethAddress?: Address | null;
btcAddress?: string | null;
solanaAddress?: string | null;
suiAddress?: string | null;

constructor(
evmProvider: PhantomEvmProvider,
bitcoinProvider: PhantomUtxoProvider,
solanaProvider: PhantomSolanaProvider
solanaProvider: PhantomSolanaProvider,
suiProvider?: PhantomSuiProvider
) {
super(evmProvider);

this.evmProvider = evmProvider;
this.bitcoinProvider = bitcoinProvider;
this.solanaProvider = solanaProvider;
this.suiProvider = suiProvider;
}

public async getDeviceID(): Promise<string> {
Expand Down Expand Up @@ -297,19 +315,17 @@ export class PhantomHDWallet

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async ethGetAddress(_msg: core.ETHGetAddress): Promise<core.Address | null> {
if (this.ethAddress) {
return this.ethAddress;
}
if (this.ethAddress) return this.ethAddress;

const address = await eth.ethGetAddress(this.evmProvider);

if (address) {
this.ethAddress = address;
return address;
} else {
this.ethAddress = null;
return null;
}

this.ethAddress = null;
return null;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -339,9 +355,34 @@ export class PhantomHDWallet
return recoverAddress(digest, msg.signature) === msg.address;
}

public async ethGetChainId(): Promise<number | null> {
try {
const chainIdHex = await this.evmProvider.request({ method: "eth_chainId" });
return parseInt(chainIdHex, 16);
} catch (error) {
console.error("Failed to get chain ID from Phantom:", error);
return null;
}
}

public async ethSwitchChain(params: core.AddEthereumChainParameter): Promise<void> {
const parsedChainId = parseInt(params.chainId, 16);
const currentChainId = await this.ethGetChainId();

if (currentChainId === parsedChainId) return;

await this.evmProvider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: params.chainId }],
});
}

/** Bitcoin */

public async btcGetAddress(msg: core.BTCGetAddress): Promise<string | null> {
// Use cached address if available to prevent rate limiting
if (this.btcAddress !== undefined) return this.btcAddress;

const value = await (async () => {
switch (msg.coin) {
case "Bitcoin": {
Expand All @@ -354,8 +395,12 @@ export class PhantomHDWallet
return null;
}
})();
if (!value || typeof value !== "string") return null;
if (!value || typeof value !== "string") {
this.btcAddress = null;
return null;
}

this.btcAddress = value;
return value;
}

Expand Down Expand Up @@ -393,8 +438,13 @@ export class PhantomHDWallet
/** Solana */

public async solanaGetAddress(): Promise<string | null> {
// Use cached address if available to prevent rate limiting
if (this.solanaAddress !== undefined) return this.solanaAddress;

const { publicKey } = await this.solanaProvider.connect();
return publicKey.toString();
const address = publicKey.toString();
this.solanaAddress = address;
return address;
}

public async solanaSignTx(msg: core.SolanaSignTx): Promise<core.SolanaSignedTx | null> {
Expand All @@ -406,4 +456,23 @@ export class PhantomHDWallet
const address = await this.solanaGetAddress();
return address ? solanaSendTx(msg, this.solanaProvider, address) : null;
}

/** Sui */

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async suiGetAddress(_msg: core.SuiGetAddress): Promise<string | null> {
if (!this.suiProvider) return null;

// Use cached address if available to prevent rate limiting
if (this.suiAddress !== undefined) return this.suiAddress;

const address = await sui.suiGetAddress(this.suiProvider);
this.suiAddress = address;
return address;
}

public async suiSignTx(msg: core.SuiSignTx): Promise<core.SuiSignedTx | null> {
if (!this.suiProvider) return null;
return sui.suiSignTx(msg, this.suiProvider);
}
}
44 changes: 44 additions & 0 deletions packages/hdwallet-phantom/src/sui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as core from "@shapeshiftoss/hdwallet-core";

import { PhantomSuiProvider } from "./types";

export async function suiGetAddress(provider: PhantomSuiProvider): Promise<string | null> {
const account = await provider.requestAccount();

if (account && account.address) return account.address;

return null;
}

export async function suiSignTx(msg: core.SuiSignTx, provider: PhantomSuiProvider): Promise<core.SuiSignedTx | null> {
const account = await provider.requestAccount();

const result = await provider.signTransaction({
transaction: msg.transactionJson,
address: account.address,
networkID: "sui:mainnet",
});

const fullSignatureBuffer = Buffer.from(result.signature, "base64");

// Phantom returns a 97-byte signature: 1 byte flag + 64 bytes signature + 32 bytes public key
if (fullSignatureBuffer.length !== 97) {
throw new Error(`Unexpected signature length: ${fullSignatureBuffer.length} bytes (expected 97)`);
}

const signatureBytes = fullSignatureBuffer.slice(1, 65);
const publicKeyBytes = fullSignatureBuffer.slice(65, 97);

const signatureHex = signatureBytes.toString("hex");
const publicKeyHex = publicKeyBytes.toString("hex");

return {
signature: signatureHex,
publicKey: publicKeyHex,
};
}

export async function suiSignMessage(message: Uint8Array, provider: PhantomSuiProvider): Promise<string | null> {
const result = await provider.signMessage(message);
return result.signature;
}
Comment thread
gomesalexandre marked this conversation as resolved.
7 changes: 7 additions & 0 deletions packages/hdwallet-phantom/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type PhantomEvmProvider = providers.ExternalProvider & {
_metamask: {
isUnlocked: () => boolean;
};
request: (args: { method: string; params?: unknown[] | object }) => Promise<unknown>;
Comment thread
gomesalexandre marked this conversation as resolved.
};

export type PhantomUtxoProvider = providers.ExternalProvider & {
Expand All @@ -30,3 +31,9 @@ export type PhantomSolanaProvider = providers.ExternalProvider & {
signTransaction(transaction: VersionedTransaction): Promise<VersionedTransaction>;
signAndSendTransaction(transaction: VersionedTransaction): Promise<{ signature: TransactionSignature }>;
};

export type PhantomSuiProvider = {
requestAccount(): Promise<{ address: string; publicKey: Uint8Array }>;
signMessage(message: Uint8Array, encoding?: string): Promise<{ signature: string }>;
signTransaction(params: { transaction: string; address: string; networkID: string }): Promise<{ signature: string }>;
};