@windoge98/pnp-solflare
Version:
Solflare Wallet adapter for PNP (Plug N Play)
308 lines (307 loc) • 12.6 kB
JavaScript
import { BaseSiwxAdapter, Adapter, deriveAccountId, formatSiwsMessage, createAdapterExtension } from "@windoge98/plug-n-play";
import { formatSiwsMessage as formatSiwsMessage2 } from "@windoge98/plug-n-play";
import { AnonymousIdentity } from "@dfinity/agent";
import { SolflareWalletAdapter } from "@solana/wallet-adapter-solflare";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { Connection } from "@solana/web3.js";
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 SolflareAdapter extends BaseSiwxAdapter {
constructor(args) {
super(args);
this.id = "solflare";
this.solanaAdapter = null;
this.solanaAddress = null;
this.connectingPromise = null;
this.handleSolanaConnect = (_publicKey) => {
this.logger.debug("Solflare connected to Solana");
};
this.handleSolanaDisconnect = () => {
this.logger.debug("Solflare disconnected from Solana");
this.setState(Adapter.Status.DISCONNECTED);
};
this.handleSolanaError = (error) => {
this.logger.error("Solflare error:", error);
this.setState(Adapter.Status.ERROR);
};
this.solanaConnection = this.initializeConnection();
this.initializeSolanaAdapter();
this.setState(Adapter.Status.READY);
}
initializeConnection() {
const network = this.config.solanaNetwork || WalletAdapterNetwork.Mainnet;
const endpoint = this.config.rpcEndpoint || (network === WalletAdapterNetwork.Mainnet ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com");
return new Connection(endpoint);
}
initializeSolanaAdapter() {
const network = this.config.solanaNetwork || WalletAdapterNetwork.Mainnet;
this.solanaAdapter = new SolflareWalletAdapter({ network });
this.setupWalletListeners();
}
setupWalletListeners() {
if (!this.solanaAdapter) return;
this.solanaAdapter.on("connect", this.handleSolanaConnect);
this.solanaAdapter.on("disconnect", this.handleSolanaDisconnect);
this.solanaAdapter.on("error", this.handleSolanaError);
}
removeWalletListeners() {
if (!this.solanaAdapter) return;
this.solanaAdapter.off("connect", this.handleSolanaConnect);
this.solanaAdapter.off("disconnect", this.handleSolanaDisconnect);
this.solanaAdapter.off("error", this.handleSolanaError);
}
async connect() {
if (this.connectingPromise) {
return this.connectingPromise;
}
this.connectingPromise = this.performConnect();
try {
const result = await this.connectingPromise;
return result;
} finally {
this.connectingPromise = null;
}
}
async performConnect() {
this.setState(Adapter.Status.CONNECTING);
try {
if (!this.solanaAdapter) {
throw new Error("Solflare adapter not initialized");
}
await this.solanaAdapter.connect();
if (!this.solanaAdapter.publicKey) {
throw new Error("Failed to get public key from Solflare");
}
const address = this.solanaAdapter.publicKey.toBase58();
this.solanaAddress = address;
const { identity, sessionKey } = await this.performSiwsLogin(address, this.solanaAdapter);
this.identity = identity;
this.sessionKey = sessionKey;
await this.storage.set(`${this.id}-solana-address`, address);
const principal = identity.getPrincipal();
this.setState(Adapter.Status.CONNECTED);
return {
owner: principal.toText(),
subaccount: deriveAccountId(principal)
};
} catch (error) {
this.setState(Adapter.Status.ERROR);
this.logger.error("Failed to connect Solflare:", error);
throw error;
}
}
async performSiwsLogin(address, adapter) {
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);
if (!adapter.signMessage) {
throw new Error("Solflare wallet does not support message signing");
}
const signatureBytes = await adapter.signMessage(messageBytes);
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() && this.solanaAdapter?.connected === true;
}
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: this.config.solanaNetwork === WalletAdapterNetwork.Devnet ? "devnet" : "mainnet"
},
icp: {
address: principal?.toText(),
subaccount: principal ? deriveAccountId(principal) : void 0
}
};
}
async disconnectInternal() {
if (this.solanaAdapter) {
try {
await this.solanaAdapter.disconnect();
} catch (error) {
this.logger.warn("Error disconnecting Solflare:", error);
}
}
this.solanaAddress = null;
await this.storage.remove(`${this.id}-solana-address`);
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) {
const storedSolanaAddress = await this.storage.get(`${this.id}-solana-address`);
if (storedSolanaAddress && typeof storedSolanaAddress === "string") {
this.solanaAddress = storedSolanaAddress;
}
}
async onClearStoredSession() {
this.solanaAddress = null;
await this.storage.remove(`${this.id}-solana-address`);
}
destroy() {
this.removeWalletListeners();
if (this.solanaAdapter) {
this.solanaAdapter.disconnect().catch(() => {
});
this.solanaAdapter = null;
}
}
}
const solflareLogo = "data:image/svg+xml,%3csvg%20width='290'%20height='290'%20viewBox='0%200%20290%20290'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20clip-path='url(%23clip0_146_299)'%3e%3cpath%20d='M63.2951%201H226.705C261.11%201%20289%2028.8905%20289%2063.2951V226.705C289%20261.11%20261.11%20289%20226.705%20289H63.2951C28.8905%20289%201%20261.11%201%20226.705V63.2951C1%2028.8905%2028.8905%201%2063.2951%201Z'%20fill='%23FFEF46'%20stroke='%23EEDA0F'%20stroke-width='2'/%3e%3cpath%20d='M140.548%20153.231L154.832%20139.432L181.462%20148.147C198.893%20153.958%20207.609%20164.61%20207.609%20179.62C207.609%20190.999%20203.251%20198.504%20194.536%20208.188L191.873%20211.093L192.841%20204.314C196.714%20179.62%20189.452%20168.968%20165.484%20161.22L140.548%20153.231ZM104.717%2068.739L177.347%2092.9488L161.61%20107.959L123.843%2095.3698C110.77%2091.012%20106.412%2083.9911%20104.717%2069.2232V68.739ZM100.359%20191.725L116.822%20175.988L147.811%20186.157C164.031%20191.483%20169.599%20198.504%20167.905%20216.177L100.359%20191.725ZM79.539%20121.516C79.539%20116.917%2081.9599%20112.559%2086.0756%20108.927C90.4334%20115.222%2097.9384%20120.79%20109.801%20124.664L135.464%20133.137L121.18%20146.937L96.0016%20138.705C84.3809%20134.832%2079.539%20129.021%2079.539%20121.516ZM155.558%20248.618C208.819%20213.272%20237.387%20189.304%20237.387%20159.768C237.387%20140.158%20225.766%20129.263%20200.104%20120.79L180.736%20114.253L233.756%2063.4128L223.103%2052.0342L207.367%2065.8337L133.043%2041.3818C110.043%2048.8869%2080.9916%2070.9178%2080.9916%2092.9487C80.9916%2095.3697%2081.2337%2097.7907%2081.96%20100.454C62.8342%20111.348%2055.0871%20121.516%2055.0871%20134.105C55.0871%20145.968%2061.3816%20157.831%2081.4758%20164.368L97.4542%20169.694L42.2559%20222.713L52.9082%20234.092L70.0972%20218.356L155.558%20248.618Z'%20fill='%2302050A'/%3e%3c/g%3e%3cdefs%3e%3cclipPath%20id='clip0_146_299'%3e%3crect%20width='290'%20height='290'%20fill='white'/%3e%3c/clipPath%3e%3c/defs%3e%3c/svg%3e";
const SolflareExtension = createAdapterExtension({
solflare: {
id: "solflare",
enabled: false,
walletName: "Solflare",
logo: solflareLogo,
website: "https://solflare.com",
chain: "SOL",
adapter: SolflareAdapter,
config: {
enabled: false,
solanaNetwork: WalletAdapterNetwork.Mainnet,
// Provider canister ID should be set by the user
siwsProviderCanisterId: ""
}
}
});
export {
SolflareAdapter,
SolflareExtension,
formatSiwsMessage2 as formatSiwsMessage
};