@windoge98/pnp-okx
Version:
OKX multi-chain wallet adapter for Plug-n-Play
377 lines (376 loc) • 14.9 kB
JavaScript
import { BaseMultiChainAdapter, BaseSiwxAdapter, Adapter, deriveAccountId, formatSiwsMessage, createAdapterExtension } from "@windoge98/plug-n-play";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { AnonymousIdentity } from "@dfinity/agent";
import { Ed25519KeyIdentity } from "@dfinity/identity";
import bs58 from "bs58";
const idlFactory = ({ IDL }) => {
const RuntimeFeature = IDL.Variant({
"IncludeUriInSeed": IDL.Null,
"DisablePrincipalToSolMapping": IDL.Null,
"DisableSolToPrincipalMapping": IDL.Null
});
IDL.Record({
"uri": IDL.Text,
"runtime_features": IDL.Opt(IDL.Vec(RuntimeFeature)),
"domain": IDL.Text,
"statement": IDL.Opt(IDL.Text),
"scheme": IDL.Opt(IDL.Text),
"salt": IDL.Text,
"session_expires_in": IDL.Opt(IDL.Nat64),
"targets": IDL.Opt(IDL.Vec(IDL.Text)),
"chain_id": IDL.Opt(IDL.Text),
"sign_in_expires_in": IDL.Opt(IDL.Nat64)
});
const Principal = IDL.Vec(IDL.Nat8);
const Address = IDL.Text;
const GetAddressResponse = IDL.Variant({ "Ok": Address, "Err": IDL.Text });
const GetPrincipalResponse = IDL.Variant({
"Ok": Principal,
"Err": IDL.Text
});
const PublicKey = IDL.Vec(IDL.Nat8);
const SessionKey = PublicKey;
const Timestamp = IDL.Nat64;
const Delegation = IDL.Record({
"pubkey": PublicKey,
"targets": IDL.Opt(IDL.Vec(IDL.Principal)),
"expiration": Timestamp
});
const SignedDelegation = IDL.Record({
"signature": IDL.Vec(IDL.Nat8),
"delegation": Delegation
});
const GetDelegationResponse = IDL.Variant({
"Ok": SignedDelegation,
"Err": IDL.Text
});
const SiwsSignature = IDL.Text;
const Nonce = IDL.Text;
const CanisterPublicKey = PublicKey;
const LoginDetails = IDL.Record({
"user_canister_pubkey": CanisterPublicKey,
"expiration": Timestamp
});
const LoginResponse = IDL.Variant({ "Ok": LoginDetails, "Err": IDL.Text });
const SiwsMessage = IDL.Record({
"uri": IDL.Text,
"issued_at": IDL.Nat64,
"domain": IDL.Text,
"statement": IDL.Text,
"version": IDL.Nat32,
"chain_id": IDL.Text,
"address": Address,
"nonce": IDL.Text,
"expiration_time": IDL.Nat64
});
const PrepareLoginResponse = IDL.Variant({
"Ok": SiwsMessage,
"Err": IDL.Text
});
return IDL.Service({
"get_address": IDL.Func([Principal], [GetAddressResponse], ["query"]),
"get_caller_address": IDL.Func([], [GetAddressResponse], ["query"]),
"get_principal": IDL.Func([Address], [GetPrincipalResponse], ["query"]),
"siws_get_delegation": IDL.Func(
[Address, SessionKey, Timestamp],
[GetDelegationResponse],
["query"]
),
"siws_login": IDL.Func(
[SiwsSignature, Address, SessionKey, Nonce],
[LoginResponse],
[]
),
"siws_prepare_login": IDL.Func([Address], [PrepareLoginResponse], [])
});
};
class OkxSolanaNetworkAdapter extends BaseSiwxAdapter {
constructor(args) {
super({
...args,
adapter: {
...args.adapter,
id: "okxSolana",
walletName: "OKX Wallet (Solana)"
}
});
this.solanaAddress = null;
this.identity = null;
this.sessionKey = null;
}
async connect() {
const win = window;
if (!win.okxwallet?.solana) {
throw new Error("OKX Wallet Solana provider not available");
}
try {
const response = await win.okxwallet.solana.connect();
const address = response.publicKey.toBase58();
this.solanaAddress = address;
const { identity, sessionKey } = await this.performSiwsLogin(address, win.okxwallet.solana);
this.identity = identity;
this.sessionKey = sessionKey;
const principal = identity.getPrincipal();
this.setState(Adapter.Status.CONNECTED);
return {
owner: principal.toText(),
subaccount: deriveAccountId(principal)
};
} catch (error) {
this.setState(Adapter.Status.ERROR);
throw error;
}
}
async performSiwsLogin(address, provider) {
const actor = await this.createSiwsProviderActor();
const prepareResult = await actor.siws_prepare_login(address);
if ("Err" in prepareResult) {
throw new Error(`SIWS Prepare Login failed: ${prepareResult.Err}`);
}
const messageText = formatSiwsMessage(prepareResult.Ok);
const messageBytes = new TextEncoder().encode(messageText);
let signatureBytes;
try {
if (typeof provider.signMessage === "function") {
const result = await provider.signMessage(messageBytes, "utf8");
if (result?.signature) {
signatureBytes = typeof result.signature === "string" ? bs58.decode(result.signature) : new Uint8Array(result.signature);
} else if (result instanceof Uint8Array) {
signatureBytes = result;
} else if (Array.isArray(result)) {
signatureBytes = new Uint8Array(result);
} else {
throw new Error("Unexpected signature format from OKX wallet");
}
} else if (typeof provider.sign === "function") {
const result = await provider.sign(messageBytes, "utf8");
signatureBytes = typeof result === "string" ? bs58.decode(result) : new Uint8Array(result);
} else if (typeof provider.request === "function") {
const result = await provider.request({
method: "signMessage",
params: [bs58.encode(messageBytes), "utf8"]
});
signatureBytes = typeof result === "string" ? bs58.decode(result) : new Uint8Array(result);
} else {
throw new Error("OKX wallet provider does not support any known message signing method");
}
} catch (error) {
throw new Error(`Failed to sign message with OKX wallet: ${error}`);
}
const signature = bs58.encode(signatureBytes);
const sessionIdentity = Ed25519KeyIdentity.generate();
const sessionPublicKeyDer = sessionIdentity.getPublicKey().toDer();
const loginResult = await actor.siws_login(
signature,
address,
new Uint8Array(sessionPublicKeyDer),
prepareResult.Ok.nonce
);
if ("Err" in loginResult) {
throw new Error(`SIWS Login failed: ${loginResult.Err}`);
}
const delegationResult = await actor.siws_get_delegation(
address,
new Uint8Array(sessionPublicKeyDer),
loginResult.Ok.expiration
);
if ("Err" in delegationResult) {
throw new Error(`SIWS Get Delegation failed: ${delegationResult.Err}`);
}
const identity = this.createDelegationIdentity(
delegationResult.Ok,
sessionIdentity,
new Uint8Array(loginResult.Ok.user_canister_pubkey).buffer
);
return { identity, sessionKey: sessionIdentity };
}
async createSiwsProviderActor(identity) {
const id = identity ?? new AnonymousIdentity();
return this.createProviderActor(idlFactory, id);
}
async isConnected() {
return this.identity !== null && !this.identity.getPrincipal().isAnonymous();
}
async getPrincipal() {
if (!this.identity) {
throw new Error("Not connected");
}
return this.identity.getPrincipal().toText();
}
async getAccountId() {
if (!this.identity) {
throw new Error("Not connected");
}
const principal = this.identity.getPrincipal();
return deriveAccountId(principal);
}
async getAddresses() {
const principal = this.identity?.getPrincipal();
return {
sol: {
address: this.solanaAddress,
network: "mainnet"
},
icp: {
address: principal?.toText(),
subaccount: principal ? deriveAccountId(principal) : void 0
}
};
}
async disconnectInternal() {
const win = window;
if (win.okxwallet?.solana) {
try {
await win.okxwallet.solana.disconnect();
} catch {
}
}
this.solanaAddress = null;
await super.disconnectInternal();
}
createActorInternal(canisterId, idl, options) {
const requiresSigning = options?.requiresSigning ?? true;
if (requiresSigning && !this.identity) {
throw new Error("Cannot create signed actor: Not connected");
}
const agent = this.buildHttpAgentSync({ identity: this.identity ?? void 0 });
return this.createActorWithAgent(agent, canisterId, idl);
}
async onStorageRestored(_sessionKey, _delegationChain) {
}
async onClearStoredSession() {
this.solanaAddress = null;
}
}
class OkxMultiChainAdapter extends BaseMultiChainAdapter {
constructor(args) {
super({
...args,
config: {
...args.config,
supportedNetworks: args.config.supportedNetworks || ["solana"]
}
});
this.networkAdapters = /* @__PURE__ */ new Map();
}
/**
* Override connect to add OKX-specific error handling
*/
async connect() {
try {
const result = await super.connect();
return result;
} catch (error) {
if (error?.message?.includes("SIWS provider")) {
const enhancedError = new Error(
`OKX Wallet connection failed: Please ensure siwsProviderCanisterId is configured in your PNP config`
);
throw enhancedError;
}
throw error;
}
}
/**
* Detect which network OKX wallet is currently connected to
*/
async detectNetwork() {
const win = window;
if (!win.okxwallet) {
throw new Error("OKX Wallet is not installed. Please install OKX Wallet browser extension.");
}
if (!win.okxwallet.solana) {
throw new Error("OKX Wallet Solana provider not available. Please ensure OKX Wallet is properly installed.");
}
return {
network: "solana",
isTestnet: false
};
}
/**
* Get or create the appropriate network-specific adapter
*/
async getNetworkAdapter(network) {
const cached = this.networkAdapters.get(network.network);
if (cached) {
return cached;
}
if (network.network === "solana") {
const adapter = new OkxSolanaNetworkAdapter({
adapter: {
...this.adapter,
chain: "SOL"
},
config: {
...this.config,
solanaNetwork: this.config?.solanaNetwork || WalletAdapterNetwork.Mainnet,
siwsProviderCanisterId: this.config?.siwsProviderCanisterId,
providerCanisterId: this.config?.siwsProviderCanisterId
},
logger: this.logger
});
this.networkAdapters.set(network.network, adapter);
return adapter;
}
throw new Error(`OKX wallet adapter only supports Solana network. Current network: ${network.network}`);
}
/**
* Switch network (OKX supports programmatic network switching for EVM chains)
*/
async switchNetwork(targetNetwork) {
const win = window;
if (!win.okxwallet) {
throw new Error("OKX Wallet not available");
}
if (["ethereum", "bsc", "polygon", "optimism", "arbitrum", "avalanche", "fantom"].includes(targetNetwork)) {
const chainIdMap = {
"ethereum": "0x1",
"bsc": "0x38",
"polygon": "0x89",
"optimism": "0xa",
"arbitrum": "0xa4b1",
"avalanche": "0xa86a",
"fantom": "0xfa"
};
const targetChainId = chainIdMap[targetNetwork];
if (!targetChainId) {
throw new Error(`Unknown chain ID for network: ${targetNetwork}`);
}
try {
await win.okxwallet.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: targetChainId }]
});
} catch (error) {
if (error.code === 4902) {
throw new Error(`Please add ${targetNetwork} network to OKX Wallet manually`);
}
throw error;
}
} else {
throw new Error(`Network switching to ${targetNetwork} must be done manually in OKX Wallet`);
}
}
}
const okxLogo = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iOCIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTI0LjA0MDkgMTQuNzAxOUgyOC44OTI1QzI5LjE3NzEgMTQuNzAxOSAyOS40MDc1IDE0LjkzMjMgMjkuNDA3NSAxNS4yMTY5VjIwLjA2ODRDMjkuNDA3NSAyMC4zNTMgMjkuMTc3MSAyMC41ODM0IDI4Ljg5MjUgMjAuNTgzNEgyNC4wNDA5QzIzLjc1NjMgMjAuNTgzNCAyMy41MjU5IDIwLjM1MyAyMy41MjU5IDIwLjA2ODRWMTUuMjE2OUMyMy41MjU5IDE0LjkzMjMgMjMuNzU2MyAxNC43MDE5IDI0LjA0MDkgMTQuNzAxOVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMC4xMDcxIDE0LjcwMTlIMTQuOTU4N0MxNS4yNDMzIDE0LjcwMTkgMTUuNDczNyAxNC45MzIzIDE1LjQ3MzcgMTUuMjE2OVYyMC4wNjg0QzE1LjQ3MzcgMjAuMzUzIDE1LjI0MzMgMjAuNTgzNCAxNC45NTg3IDIwLjU4MzRIMTAuMTA3MUM5LjgyMjUgMjAuNTgzNCA5LjU5MjEgMjAuMzUzIDkuNTkyMSAyMC4wNjg0VjE1LjIxNjlDOS41OTIxIDE0LjkzMjMgOS44MjI1IDE0LjcwMTkgMTAuMTA3MSAxNC43MDE5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTI0LjA0MDkgNS41SDI4Ljg5MjVDMjkuMTc3MSA1LjUgMjkuNDA3NSA1LjczMDQgMjkuNDA3NSA2LjAxNVYxMC44NjY1QzI5LjQwNzUgMTEuMTUxMSAyOS4xNzcxIDExLjM4MTUgMjguODkyNSAxMS4zODE1SDI0LjA0MDlDMjMuNzU2MyAxMS4zODE1IDIzLjUyNTkgMTEuMTUxMSAyMy41MjU5IDEwLjg2NjVWNi4wMTVDMjMuNTI1OSA1LjczMDQgMjMuNzU2MyA1LjUgMjQuMDQwOSA1LjVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAuMTA3MSA1LjVIMTQuOTU4N0MxNS4yNDMzIDUuNSAxNS40NzM3IDUuNzMwNCAxNS40NzM3IDYuMDE1VjEwLjg2NjVDMTUuNDczNyAxMS4xNTExIDE1LjI0MzMgMTEuMzgxNSAxNC45NTg3IDExLjM4MTVIMTAuMTA3MUM5LjgyMjUgMTEuMzgxNSA5LjU5MjEgMTEuMTUxMSA5LjU5MjEgMTAuODY2NVY2LjAxNUM5LjU5MjEgNS43MzA0IDkuODIyNSA1LjUgMTAuMTA3MSA1LjVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjQuMDQwOSAyMy45MDM3SDI4Ljg5MjVDMjkuMTc3MSAyMy45MDM3IDI5LjQwNzUgMjQuMTM0MSAyOS40MDc1IDI0LjQxODdWMjkuMjcwM0MyOS40MDc1IDI5LjU1NDkgMjkuMTc3MSAyOS43ODUzIDI4Ljg5MjUgMjkuNzg1M0gyNC4wNDA5QzIzLjc1NjMgMjkuNzg1MyAyMy41MjU5IDI5LjU1NDkgMjMuNTI1OSAyOS4yNzAzVjI0LjQxODdDMjMuNTI1OSAyNC4xMzQxIDIzLjc1NjMgMjMuOTAzNyAyNC4wNDA5IDIzLjkwMzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAuMTA3MSAyMy45MDM3SDE0Ljk1ODdDMTUuMjQzMyAyMy45MDM3IDE1LjQ3MzcgMjQuMTM0MSAxNS40NzM3IDI0LjQxODdWMjkuMjcwM0MxNS40NzM3IDI5LjU1NDkgMTUuMjQzMyAyOS43ODUzIDE0Ljk1ODcgMjkuNzg1M0gxMC4xMDcxQzkuODIyNSAyOS43ODUzIDkuNTkyMSAyOS41NTQ5IDkuNTkyMSAyOS4yNzAzVjI0LjQxODdDOS41OTIxIDI0LjEzNDEgOS44MjI1IDIzLjkwMzcgMTAuMTA3MSAyMy45MDM3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE3LjA3NCAxNC43MDE5SDIxLjkyNTVDMjIuMjEwMSAxNC43MDE5IDIyLjQ0MDUgMTQuOTMyMyAyMi40NDA1IDE1LjIxNjlWMjAuMDY4NEMyMi40NDA1IDIwLjM1MyAyMi4yMTAxIDIwLjU4MzQgMjEuOTI1NSAyMC41ODM0SDE3LjA3NEMxNi43ODk0IDIwLjU4MzQgMTYuNTU5IDIwLjM1MyAxNi41NTkgMjAuMDY4NFYxNS4yMTY5QzE2LjU1OSAxNC45MzIzIDE2Ljc4OTQgMTQuNzAxOSAxNy4wNzQgMTQuNzAxOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==";
const OkxExtension = createAdapterExtension({
okx: {
id: "okx",
enabled: false,
walletName: "OKX Wallet",
logo: okxLogo,
website: "https://www.okx.com/web3",
chain: "SOL",
adapter: OkxMultiChainAdapter,
config: {
enabled: false,
supportedNetworks: ["solana"],
solanaNetwork: WalletAdapterNetwork.Mainnet,
// Provider canister ID should be set by the user
siwsProviderCanisterId: ""
}
}
});
export {
OkxExtension,
OkxMultiChainAdapter
};