FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,914 @@
import { getBase58Decoder } from "@solana/codecs-strings";
import { createSignInMessageText } from "@solana/wallet-standard-util";
//#region src/errors.ts
const SolanaMobileWalletAdapterErrorCode = {
ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: "ERROR_ASSOCIATION_PORT_OUT_OF_RANGE",
ERROR_REFLECTOR_ID_OUT_OF_RANGE: "ERROR_REFLECTOR_ID_OUT_OF_RANGE",
ERROR_FORBIDDEN_WALLET_BASE_URL: "ERROR_FORBIDDEN_WALLET_BASE_URL",
ERROR_SECURE_CONTEXT_REQUIRED: "ERROR_SECURE_CONTEXT_REQUIRED",
ERROR_SESSION_CLOSED: "ERROR_SESSION_CLOSED",
ERROR_SESSION_TIMEOUT: "ERROR_SESSION_TIMEOUT",
ERROR_WALLET_NOT_FOUND: "ERROR_WALLET_NOT_FOUND",
ERROR_INVALID_PROTOCOL_VERSION: "ERROR_INVALID_PROTOCOL_VERSION",
ERROR_BROWSER_NOT_SUPPORTED: "ERROR_BROWSER_NOT_SUPPORTED",
ERROR_LOOPBACK_ACCESS_BLOCKED: "ERROR_LOOPBACK_ACCESS_BLOCKED",
ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED"
};
var SolanaMobileWalletAdapterError = class extends Error {
data;
code;
constructor(...args) {
const [code, message, data] = args;
super(message);
this.code = code;
this.data = data;
this.name = "SolanaMobileWalletAdapterError";
}
};
const SolanaMobileWalletAdapterProtocolErrorCode = {
ERROR_AUTHORIZATION_FAILED: -1,
ERROR_INVALID_PAYLOADS: -2,
ERROR_NOT_SIGNED: -3,
ERROR_NOT_SUBMITTED: -4,
ERROR_TOO_MANY_PAYLOADS: -5,
ERROR_ATTEST_ORIGIN_ANDROID: -100
};
var SolanaMobileWalletAdapterProtocolError = class extends Error {
data;
code;
jsonRpcMessageId;
constructor(...args) {
const [jsonRpcMessageId, code, message, data] = args;
super(message);
this.code = code;
this.data = data;
this.jsonRpcMessageId = jsonRpcMessageId;
this.name = "SolanaMobileWalletAdapterProtocolError";
}
};
//#endregion
//#region src/base64Utils.ts
function encode(input) {
return window.btoa(input);
}
function fromUint8Array$1(byteArray, urlsafe) {
const base64 = window.btoa(String.fromCharCode.call(null, ...byteArray));
if (urlsafe) return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
else return base64;
}
function toUint8Array(base64EncodedByteArray) {
return new Uint8Array(window.atob(base64EncodedByteArray).split("").map((c) => c.charCodeAt(0)));
}
//#endregion
//#region src/createHelloReq.ts
async function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
const publicKeyBuffer = await crypto.subtle.exportKey("raw", ecdhPublicKey);
const signatureBuffer = await crypto.subtle.sign({
hash: "SHA-256",
name: "ECDSA"
}, associationKeypairPrivateKey, publicKeyBuffer);
const response = new Uint8Array(publicKeyBuffer.byteLength + signatureBuffer.byteLength);
response.set(new Uint8Array(publicKeyBuffer), 0);
response.set(new Uint8Array(signatureBuffer), publicKeyBuffer.byteLength);
return response;
}
//#endregion
//#region src/base58Utils.ts
function fromUint8Array(byteArray) {
return getBase58Decoder().decode(byteArray);
}
function base64ToBase58(base64EncodedString) {
return fromUint8Array(toUint8Array(base64EncodedString));
}
//#endregion
//#region src/createSIWSMessage.ts
function createSIWSMessage(payload) {
return createSignInMessageText(payload);
}
function createSIWSMessageBase64Url(payload) {
return encode(createSIWSMessage(payload)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
//#endregion
//#region src/types.ts
const SolanaSignTransactions = "solana:signTransactions";
const SolanaCloneAuthorization = "solana:cloneAuthorization";
const SolanaSignInWithSolana = "solana:signInWithSolana";
//#endregion
//#region src/createMobileWalletProxy.ts
/**
* Creates a {@link MobileWallet} proxy that handles backwards compatibility and API to RPC conversion.
*
* @param protocolVersion the protocol version in use for this session/request
* @param protocolRequestHandler callback function that handles sending the RPC request to the wallet endpoint.
* @returns a {@link MobileWallet} proxy
*/
function createMobileWalletProxy(protocolVersion, protocolRequestHandler) {
return new Proxy({}, {
get(target, p) {
if (p === "then") return null;
if (target[p] == null) target[p] = async function(inputParams) {
const { method, params } = handleMobileWalletRequest(p, inputParams, protocolVersion);
const result = await protocolRequestHandler(method, params);
if (method === "authorize" && params.sign_in_payload && !result.sign_in_result) result.sign_in_result = await signInFallback(params.sign_in_payload, result, protocolRequestHandler);
return handleMobileWalletResponse(p, result, protocolVersion);
};
return target[p];
},
defineProperty() {
return false;
},
deleteProperty() {
return false;
}
});
}
/**
* Handles all {@link MobileWallet} API requests and determines the correct MWA RPC method and params to call.
* This handles backwards compatibility, based on the provided @protocolVersion.
*
* @param methodName the name of {@link MobileWallet} method that was called
* @param methodParams the parameters that were passed to the method
* @param protocolVersion the protocol version in use for this session/request
* @returns the RPC request method and params that should be sent to the wallet endpoint
*/
function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
let params = methodParams;
let method = methodName.toString().replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).toLowerCase();
switch (methodName) {
case "authorize": {
const authorizeParams = params;
let { chain } = authorizeParams;
if (protocolVersion === "legacy") {
switch (chain) {
case "solana:testnet":
chain = "testnet";
break;
case "solana:devnet":
chain = "devnet";
break;
case "solana:mainnet":
chain = "mainnet-beta";
break;
default: chain = authorizeParams.cluster;
}
authorizeParams.cluster = chain;
params = authorizeParams;
} else {
switch (chain) {
case "testnet":
case "devnet":
chain = `solana:${chain}`;
break;
case "mainnet-beta":
chain = "solana:mainnet";
break;
}
authorizeParams.chain = chain;
params = authorizeParams;
}
}
case "reauthorize": {
const { auth_token, identity } = params;
if (auth_token) switch (protocolVersion) {
case "legacy":
method = "reauthorize";
params = {
auth_token,
identity
};
break;
default:
method = "authorize";
break;
}
break;
}
}
return {
method,
params
};
}
/**
* Handles all {@link MobileWallet} API responses and modifies the response for backwards compatibility, if needed
*
* @param method the {@link MobileWallet} method that was called
* @param response the original response that was returned by the method call
* @param protocolVersion the protocol version in use for this session/request
* @returns the possibly modified response
*/
function handleMobileWalletResponse(method, response, protocolVersion) {
switch (method) {
case "getCapabilities": {
const capabilities = response;
switch (protocolVersion) {
case "legacy": {
const features = [SolanaSignTransactions];
if (capabilities.supports_clone_authorization === true) features.push(SolanaCloneAuthorization);
return {
...capabilities,
features
};
}
case "v1": return {
...capabilities,
supports_sign_and_send_transactions: true,
supports_clone_authorization: capabilities.features.includes(SolanaCloneAuthorization)
};
}
}
}
return response;
}
async function signInFallback(signInPayload, authorizationResult, protocolRequestHandler) {
const domain = signInPayload.domain ?? window.location.host;
const address = authorizationResult.accounts[0].address;
const siwsMessage = createSIWSMessageBase64Url({
...signInPayload,
domain,
address: base64ToBase58(address)
});
const signedPayload = toUint8Array((await protocolRequestHandler("sign_messages", {
addresses: [address],
payloads: [siwsMessage]
})).signed_payloads[0]);
const signedMessage = fromUint8Array$1(signedPayload.slice(0, signedPayload.length - 64));
const signature = fromUint8Array$1(signedPayload.slice(signedPayload.length - 64));
return {
address,
signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
signature
};
}
function createSequenceNumberVector(sequenceNumber) {
if (sequenceNumber >= 4294967296) throw new Error("Outbound sequence number overflow. The maximum sequence number is 32-bytes.");
const byteArray = /* @__PURE__ */ new ArrayBuffer(4);
new DataView(byteArray).setUint32(0, sequenceNumber, false);
return new Uint8Array(byteArray);
}
//#endregion
//#region src/encryptedMessage.ts
const INITIALIZATION_VECTOR_BYTES = 12;
async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
const sequenceNumberVector = createSequenceNumberVector(sequenceNumber);
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
crypto.getRandomValues(initializationVector);
const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, new TextEncoder().encode(plaintext));
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
response.set(new Uint8Array(sequenceNumberVector), 0);
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
response.set(new Uint8Array(ciphertext), sequenceNumberVector.byteLength + initializationVector.byteLength);
return response;
}
async function decryptMessage(message, sharedSecret) {
const sequenceNumberVector = message.slice(0, 4);
const initializationVector = message.slice(4, 4 + INITIALIZATION_VECTOR_BYTES);
const ciphertext = message.slice(4 + INITIALIZATION_VECTOR_BYTES);
const plaintextBuffer = await crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
return getUtf8Decoder().decode(plaintextBuffer);
}
function getAlgorithmParams(sequenceNumber, initializationVector) {
return {
additionalData: sequenceNumber,
iv: initializationVector,
name: "AES-GCM",
tagLength: 128
};
}
let _utf8Decoder;
function getUtf8Decoder() {
if (_utf8Decoder === void 0) _utf8Decoder = new TextDecoder("utf-8");
return _utf8Decoder;
}
//#endregion
//#region src/generateAssociationKeypair.ts
async function generateAssociationKeypair() {
return await crypto.subtle.generateKey({
name: "ECDSA",
namedCurve: "P-256"
}, false, ["sign"]);
}
//#endregion
//#region src/generateECDHKeypair.ts
async function generateECDHKeypair() {
return await crypto.subtle.generateKey({
name: "ECDH",
namedCurve: "P-256"
}, false, ["deriveKey", "deriveBits"]);
}
//#endregion
//#region src/arrayBufferToBase64String.ts
function arrayBufferToBase64String(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let ii = 0; ii < len; ii++) binary += String.fromCharCode(bytes[ii]);
return window.btoa(binary);
}
//#endregion
//#region src/associationPort.ts
function getRandomAssociationPort() {
return assertAssociationPort(49152 + Math.floor(Math.random() * 16384));
}
function assertAssociationPort(port) {
if (port < 49152 || port > 65535) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${port} given.`, { port });
return port;
}
//#endregion
//#region src/getStringWithURLUnsafeBase64CharactersReplaced.ts
function getStringWithURLUnsafeCharactersReplaced(unsafeBase64EncodedString) {
return unsafeBase64EncodedString.replace(/[/+=]/g, (m) => ({
"/": "_",
"+": "-",
"=": "."
})[m]);
}
//#endregion
//#region src/getAssociateAndroidIntentURL.ts
const INTENT_NAME = "solana-wallet";
function getPathParts(pathString) {
return pathString.replace(/(^\/+|\/+$)/g, "").split("/");
}
function getIntentURL(methodPathname, intentUrlBase) {
let baseUrl = null;
if (intentUrlBase) {
try {
baseUrl = new URL(intentUrlBase);
} catch {}
if (baseUrl?.protocol !== "https:") throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, "Base URLs supplied by wallets must be valid `https` URLs");
}
baseUrl ||= new URL(`${INTENT_NAME}:/`);
const pathname = methodPathname.startsWith("/") ? methodPathname : [...getPathParts(baseUrl.pathname), ...getPathParts(methodPathname)].join("/");
return new URL(pathname, baseUrl);
}
async function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ["v1"]) {
const associationPort = assertAssociationPort(putativePort);
const encodedKey = arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
const url = getIntentURL("v1/associate/local", associationURLBase);
url.searchParams.set("association", getStringWithURLUnsafeCharactersReplaced(encodedKey));
url.searchParams.set("port", `${associationPort}`);
protocolVersions.forEach((version) => {
url.searchParams.set("v", version);
});
return url;
}
async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ["v1"]) {
const encodedKey = arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
const url = getIntentURL("v1/associate/remote", associationURLBase);
url.searchParams.set("association", getStringWithURLUnsafeCharactersReplaced(encodedKey));
url.searchParams.set("reflector", `${hostAuthority}`);
url.searchParams.set("id", `${fromUint8Array$1(reflectorId, true)}`);
protocolVersions.forEach((version) => {
url.searchParams.set("v", version);
});
return url;
}
//#endregion
//#region src/jsonRpcMessage.ts
async function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
const plaintext = JSON.stringify(jsonRpcMessage);
const sequenceNumber = jsonRpcMessage.id;
return encryptMessage(plaintext, sequenceNumber, sharedSecret);
}
async function decryptJsonRpcMessage(message, sharedSecret) {
const plaintext = await decryptMessage(message, sharedSecret);
const jsonRpcMessage = JSON.parse(plaintext);
if (Object.hasOwnProperty.call(jsonRpcMessage, "error")) throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
return jsonRpcMessage;
}
//#endregion
//#region src/parseHelloRsp.ts
async function parseHelloRsp(payloadBuffer, associationPublicKey, ecdhPrivateKey) {
const [associationPublicKeyBuffer, walletPublicKey] = await Promise.all([crypto.subtle.exportKey("raw", associationPublicKey), crypto.subtle.importKey("raw", payloadBuffer.slice(0, 65), {
name: "ECDH",
namedCurve: "P-256"
}, false, [])]);
const sharedSecret = await crypto.subtle.deriveBits({
name: "ECDH",
public: walletPublicKey
}, ecdhPrivateKey, 256);
const ecdhSecretKey = await crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"]);
return await crypto.subtle.deriveKey({
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(associationPublicKeyBuffer),
info: new Uint8Array()
}, ecdhSecretKey, {
name: "AES-GCM",
length: 128
}, false, ["encrypt", "decrypt"]);
}
//#endregion
//#region src/parseSessionProps.ts
async function parseSessionProps(message, sharedSecret) {
const plaintext = await decryptMessage(message, sharedSecret);
const jsonProperties = JSON.parse(plaintext);
let protocolVersion = "legacy";
if (Object.hasOwnProperty.call(jsonProperties, "v")) switch (jsonProperties.v) {
case 1:
case "1":
case "v1":
protocolVersion = "v1";
break;
case "legacy":
protocolVersion = "legacy";
break;
default: throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION, `Unknown/unsupported protocol version: ${jsonProperties.v}`);
}
return { protocol_version: protocolVersion };
}
//#endregion
//#region src/startSession.ts
const Browser = {
Firefox: 0,
Other: 1
};
function assertUnreachable(x) {
return x;
}
function getBrowser() {
return navigator.userAgent.indexOf("Firefox/") !== -1 ? Browser.Firefox : Browser.Other;
}
function getDetectionPromise() {
return new Promise((resolve, reject) => {
function cleanup() {
clearTimeout(timeoutId);
window.removeEventListener("blur", handleBlur);
}
function handleBlur() {
cleanup();
resolve();
}
window.addEventListener("blur", handleBlur);
const timeoutId = setTimeout(() => {
cleanup();
reject();
}, 3e3);
});
}
let _frame = null;
function launchUrlThroughHiddenFrame(url) {
if (_frame == null) {
_frame = document.createElement("iframe");
_frame.style.display = "none";
document.body.appendChild(_frame);
}
_frame.contentWindow.location.href = url.toString();
}
async function launchAssociation(associationUrl) {
if (associationUrl.protocol === "https:") window.location.assign(associationUrl);
else try {
const browser = getBrowser();
switch (browser) {
case Browser.Firefox:
launchUrlThroughHiddenFrame(associationUrl);
break;
case Browser.Other: {
const detectionPromise = getDetectionPromise();
window.location.assign(associationUrl);
await detectionPromise;
break;
}
default: assertUnreachable(browser);
}
} catch {
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, "Found no installed wallet that supports the mobile wallet protocol.");
}
}
async function startSession(associationPublicKey, associationURLBase) {
const randomAssociationPort = getRandomAssociationPort();
await launchAssociation(await getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase));
return randomAssociationPort;
}
//#endregion
//#region src/transact.ts
const WEBSOCKET_CONNECTION_CONFIG = {
retryDelayScheduleMs: [
150,
150,
200,
500,
500,
750,
750,
1e3
],
timeoutMs: 3e4
};
const WEBSOCKET_PROTOCOL_BINARY = "com.solana.mobilewalletadapter.v1";
const WEBSOCKET_PROTOCOL_BASE64 = "com.solana.mobilewalletadapter.v1.base64";
function assertSecureContext() {
if (typeof window === "undefined" || window.isSecureContext !== true) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SECURE_CONTEXT_REQUIRED, "The mobile wallet adapter protocol must be used in a secure context (`https`).");
}
function assertSecureEndpointSpecificURI(walletUriBase) {
let url;
try {
url = new URL(walletUriBase);
} catch {
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, "Invalid base URL supplied by wallet");
}
if (url.protocol !== "https:") throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, "Base URLs supplied by wallets must be valid `https` URLs");
}
function getSequenceNumberFromByteArray(byteArray) {
return new DataView(byteArray).getUint32(0, false);
}
function decodeVarLong(byteArray) {
const bytes = new Uint8Array(byteArray);
const l = byteArray.byteLength;
const limit = 10;
let value = 0, offset = 0, b;
do {
if (offset >= l || offset > limit) throw new RangeError("Failed to decode varint");
b = bytes[offset++];
value |= (b & 127) << 7 * offset;
} while (b >= 128);
return {
value,
offset
};
}
function getReflectorIdFromByteArray(byteArray) {
const { value: length, offset } = decodeVarLong(byteArray);
return new Uint8Array(byteArray.slice(offset, offset + length));
}
async function transact(callback, config) {
const { wallet, close } = await startScenario(config);
try {
return await callback(await wallet);
} finally {
close();
}
}
async function startScenario(config) {
assertSecureContext();
const associationKeypair = await generateAssociationKeypair();
const websocketURL = `ws://localhost:${await startSession(associationKeypair.publicKey, config?.baseUri)}/solana-wallet`;
let connectionStartTime;
const getNextRetryDelayMs = (() => {
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
return () => schedule.length > 1 ? schedule.shift() : schedule[0];
})();
let nextJsonRpcMessageId = 1;
let lastKnownInboundSequenceNumber = 0;
let state = { __type: "disconnected" };
let socket;
let sessionEstablished = false;
let handleForceClose;
return {
close: () => {
socket.close();
handleForceClose();
},
wallet: new Promise((resolve, reject) => {
const jsonRpcResponsePromises = {};
const handleOpen = async () => {
if (state.__type !== "connecting") {
console.warn(`Expected adapter state to be \`connecting\` at the moment the websocket opens. Got \`${state.__type}\`.`);
return;
}
socket.removeEventListener("open", handleOpen);
const { associationKeypair } = state;
const ecdhKeypair = await generateECDHKeypair();
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
};
const handleClose = (evt) => {
if (evt.wasClean) state = { __type: "disconnected" };
else reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
disposeSocket();
};
const handleError = async (_evt) => {
disposeSocket();
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
else {
await new Promise((resolve) => {
const retryDelayMs = getNextRetryDelayMs();
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
});
attemptSocketConnection();
}
};
const handleMessage = async (evt) => {
const responseBuffer = await evt.data.arrayBuffer();
switch (state.__type) {
case "connecting": {
if (responseBuffer.byteLength !== 0) throw new Error("Encountered unexpected message while connecting");
const ecdhKeypair = await generateECDHKeypair();
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
break;
}
case "connected":
try {
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
delete jsonRpcResponsePromises[jsonRpcMessage.id];
responsePromise.resolve(jsonRpcMessage.result);
} catch (e) {
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
responsePromise.reject(e);
} else throw e;
}
break;
case "hello_req_sent": {
if (responseBuffer.byteLength === 0) {
const ecdhKeypair = await generateECDHKeypair();
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
break;
}
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
const sessionPropertiesBuffer = responseBuffer.slice(65);
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
})() : { protocol_version: "legacy" };
state = {
__type: "connected",
sharedSecret,
sessionProperties
};
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
const id = nextJsonRpcMessageId++;
socket.send(await encryptJsonRpcMessage({
id,
jsonrpc: "2.0",
method,
params: params ?? {}
}, sharedSecret));
return new Promise((resolve, reject) => {
jsonRpcResponsePromises[id] = {
resolve(result) {
switch (method) {
case "authorize":
case "reauthorize": {
const { wallet_uri_base } = result;
if (wallet_uri_base != null) try {
assertSecureEndpointSpecificURI(wallet_uri_base);
} catch (e) {
reject(e);
return;
}
break;
}
}
resolve(result);
},
reject
};
});
});
sessionEstablished = true;
try {
resolve(wallet);
} catch (e) {
reject(e);
}
break;
}
}
};
handleForceClose = () => {
socket.removeEventListener("message", handleMessage);
disposeSocket();
if (!sessionEstablished) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent("socket was closed before connection") }));
};
let disposeSocket;
let retryWaitTimeoutId;
const attemptSocketConnection = () => {
if (disposeSocket) disposeSocket();
state = {
__type: "connecting",
associationKeypair
};
if (connectionStartTime === void 0) connectionStartTime = Date.now();
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL_BINARY]);
socket.addEventListener("open", handleOpen);
socket.addEventListener("close", handleClose);
socket.addEventListener("error", handleError);
socket.addEventListener("message", handleMessage);
disposeSocket = () => {
window.clearTimeout(retryWaitTimeoutId);
socket.removeEventListener("open", handleOpen);
socket.removeEventListener("close", handleClose);
socket.removeEventListener("error", handleError);
socket.removeEventListener("message", handleMessage);
};
};
attemptSocketConnection();
})
};
}
async function startRemoteScenario(config) {
assertSecureContext();
const associationKeypair = await generateAssociationKeypair();
const websocketURL = `wss://${config?.remoteHostAuthority}/reflect`;
let connectionStartTime;
const getNextRetryDelayMs = (() => {
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
return () => schedule.length > 1 ? schedule.shift() : schedule[0];
})();
let nextJsonRpcMessageId = 1;
let lastKnownInboundSequenceNumber = 0;
let encoding;
let state = { __type: "disconnected" };
let socket;
let disposeSocket;
const decodeBytes = async (evt) => {
if (encoding == "base64") return toUint8Array(await evt.data).buffer;
else return await evt.data.arrayBuffer();
};
const associationUrl = await new Promise((resolve, reject) => {
const handleOpen = async () => {
if (state.__type !== "connecting") {
console.warn(`Expected adapter state to be \`connecting\` at the moment the websocket opens. Got \`${state.__type}\`.`);
return;
}
if (socket.protocol.includes(WEBSOCKET_PROTOCOL_BASE64)) encoding = "base64";
else encoding = "binary";
socket.removeEventListener("open", handleOpen);
};
const handleClose = (evt) => {
if (evt.wasClean) state = { __type: "disconnected" };
else reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
disposeSocket();
};
const handleError = async (_evt) => {
disposeSocket();
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
else {
await new Promise((resolve) => {
const retryDelayMs = getNextRetryDelayMs();
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
});
attemptSocketConnection();
}
};
const handleReflectorIdMessage = async (evt) => {
const responseBuffer = await decodeBytes(evt);
if (state.__type === "connecting") {
if (responseBuffer.byteLength == 0) throw new Error("Encountered unexpected message while connecting");
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
state = {
__type: "reflector_id_received",
reflectorId
};
const associationUrl = await getRemoteAssociateAndroidIntentURL(associationKeypair.publicKey, config.remoteHostAuthority, reflectorId, config?.baseUri);
socket.removeEventListener("message", handleReflectorIdMessage);
resolve(associationUrl);
}
};
let retryWaitTimeoutId;
const attemptSocketConnection = () => {
if (disposeSocket) disposeSocket();
state = {
__type: "connecting",
associationKeypair
};
if (connectionStartTime === void 0) connectionStartTime = Date.now();
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL_BINARY, WEBSOCKET_PROTOCOL_BASE64]);
socket.addEventListener("open", handleOpen);
socket.addEventListener("close", handleClose);
socket.addEventListener("error", handleError);
socket.addEventListener("message", handleReflectorIdMessage);
disposeSocket = () => {
window.clearTimeout(retryWaitTimeoutId);
socket.removeEventListener("open", handleOpen);
socket.removeEventListener("close", handleClose);
socket.removeEventListener("error", handleError);
socket.removeEventListener("message", handleReflectorIdMessage);
};
};
attemptSocketConnection();
});
let sessionEstablished = false;
let handleClose;
return {
associationUrl,
close: () => {
socket.close();
handleClose();
},
wallet: new Promise((resolve, reject) => {
const jsonRpcResponsePromises = {};
const handleMessage = async (evt) => {
const responseBuffer = await decodeBytes(evt);
switch (state.__type) {
case "reflector_id_received": {
if (responseBuffer.byteLength !== 0) throw new Error("Encountered unexpected message while awaiting reflection");
const ecdhKeypair = await generateECDHKeypair();
const binaryMsg = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
if (encoding == "base64") socket.send(fromUint8Array$1(binaryMsg));
else socket.send(binaryMsg);
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
break;
}
case "connected":
try {
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
delete jsonRpcResponsePromises[jsonRpcMessage.id];
responsePromise.resolve(jsonRpcMessage.result);
} catch (e) {
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
responsePromise.reject(e);
} else throw e;
}
break;
case "hello_req_sent": {
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
const sessionPropertiesBuffer = responseBuffer.slice(65);
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
})() : { protocol_version: "legacy" };
state = {
__type: "connected",
sharedSecret,
sessionProperties
};
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
const id = nextJsonRpcMessageId++;
const binaryMsg = await encryptJsonRpcMessage({
id,
jsonrpc: "2.0",
method,
params: params ?? {}
}, sharedSecret);
if (encoding == "base64") socket.send(fromUint8Array$1(binaryMsg));
else socket.send(binaryMsg);
return new Promise((resolve, reject) => {
jsonRpcResponsePromises[id] = {
resolve(result) {
switch (method) {
case "authorize":
case "reauthorize": {
const { wallet_uri_base } = result;
if (wallet_uri_base != null) try {
assertSecureEndpointSpecificURI(wallet_uri_base);
} catch (e) {
reject(e);
return;
}
break;
}
}
resolve(result);
},
reject
};
});
});
sessionEstablished = true;
try {
resolve(wallet);
} catch (e) {
reject(e);
}
break;
}
}
};
socket.addEventListener("message", handleMessage);
handleClose = () => {
socket.removeEventListener("message", handleMessage);
disposeSocket();
if (!sessionEstablished) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent("socket was closed before connection") }));
};
})
};
}
//#endregion
export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, startRemoteScenario, startScenario, transact };
//# sourceMappingURL=index.browser.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,914 @@
import { getBase58Decoder } from "@solana/codecs-strings";
import { createSignInMessageText } from "@solana/wallet-standard-util";
//#region src/errors.ts
const SolanaMobileWalletAdapterErrorCode = {
ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: "ERROR_ASSOCIATION_PORT_OUT_OF_RANGE",
ERROR_REFLECTOR_ID_OUT_OF_RANGE: "ERROR_REFLECTOR_ID_OUT_OF_RANGE",
ERROR_FORBIDDEN_WALLET_BASE_URL: "ERROR_FORBIDDEN_WALLET_BASE_URL",
ERROR_SECURE_CONTEXT_REQUIRED: "ERROR_SECURE_CONTEXT_REQUIRED",
ERROR_SESSION_CLOSED: "ERROR_SESSION_CLOSED",
ERROR_SESSION_TIMEOUT: "ERROR_SESSION_TIMEOUT",
ERROR_WALLET_NOT_FOUND: "ERROR_WALLET_NOT_FOUND",
ERROR_INVALID_PROTOCOL_VERSION: "ERROR_INVALID_PROTOCOL_VERSION",
ERROR_BROWSER_NOT_SUPPORTED: "ERROR_BROWSER_NOT_SUPPORTED",
ERROR_LOOPBACK_ACCESS_BLOCKED: "ERROR_LOOPBACK_ACCESS_BLOCKED",
ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED"
};
var SolanaMobileWalletAdapterError = class extends Error {
data;
code;
constructor(...args) {
const [code, message, data] = args;
super(message);
this.code = code;
this.data = data;
this.name = "SolanaMobileWalletAdapterError";
}
};
const SolanaMobileWalletAdapterProtocolErrorCode = {
ERROR_AUTHORIZATION_FAILED: -1,
ERROR_INVALID_PAYLOADS: -2,
ERROR_NOT_SIGNED: -3,
ERROR_NOT_SUBMITTED: -4,
ERROR_TOO_MANY_PAYLOADS: -5,
ERROR_ATTEST_ORIGIN_ANDROID: -100
};
var SolanaMobileWalletAdapterProtocolError = class extends Error {
data;
code;
jsonRpcMessageId;
constructor(...args) {
const [jsonRpcMessageId, code, message, data] = args;
super(message);
this.code = code;
this.data = data;
this.jsonRpcMessageId = jsonRpcMessageId;
this.name = "SolanaMobileWalletAdapterProtocolError";
}
};
//#endregion
//#region src/base64Utils.ts
function encode(input) {
return window.btoa(input);
}
function fromUint8Array$1(byteArray, urlsafe) {
const base64 = window.btoa(String.fromCharCode.call(null, ...byteArray));
if (urlsafe) return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
else return base64;
}
function toUint8Array(base64EncodedByteArray) {
return new Uint8Array(window.atob(base64EncodedByteArray).split("").map((c) => c.charCodeAt(0)));
}
//#endregion
//#region src/createHelloReq.ts
async function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
const publicKeyBuffer = await crypto.subtle.exportKey("raw", ecdhPublicKey);
const signatureBuffer = await crypto.subtle.sign({
hash: "SHA-256",
name: "ECDSA"
}, associationKeypairPrivateKey, publicKeyBuffer);
const response = new Uint8Array(publicKeyBuffer.byteLength + signatureBuffer.byteLength);
response.set(new Uint8Array(publicKeyBuffer), 0);
response.set(new Uint8Array(signatureBuffer), publicKeyBuffer.byteLength);
return response;
}
//#endregion
//#region src/base58Utils.ts
function fromUint8Array(byteArray) {
return getBase58Decoder().decode(byteArray);
}
function base64ToBase58(base64EncodedString) {
return fromUint8Array(toUint8Array(base64EncodedString));
}
//#endregion
//#region src/createSIWSMessage.ts
function createSIWSMessage(payload) {
return createSignInMessageText(payload);
}
function createSIWSMessageBase64Url(payload) {
return encode(createSIWSMessage(payload)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
//#endregion
//#region src/types.ts
const SolanaSignTransactions = "solana:signTransactions";
const SolanaCloneAuthorization = "solana:cloneAuthorization";
const SolanaSignInWithSolana = "solana:signInWithSolana";
//#endregion
//#region src/createMobileWalletProxy.ts
/**
* Creates a {@link MobileWallet} proxy that handles backwards compatibility and API to RPC conversion.
*
* @param protocolVersion the protocol version in use for this session/request
* @param protocolRequestHandler callback function that handles sending the RPC request to the wallet endpoint.
* @returns a {@link MobileWallet} proxy
*/
function createMobileWalletProxy(protocolVersion, protocolRequestHandler) {
return new Proxy({}, {
get(target, p) {
if (p === "then") return null;
if (target[p] == null) target[p] = async function(inputParams) {
const { method, params } = handleMobileWalletRequest(p, inputParams, protocolVersion);
const result = await protocolRequestHandler(method, params);
if (method === "authorize" && params.sign_in_payload && !result.sign_in_result) result.sign_in_result = await signInFallback(params.sign_in_payload, result, protocolRequestHandler);
return handleMobileWalletResponse(p, result, protocolVersion);
};
return target[p];
},
defineProperty() {
return false;
},
deleteProperty() {
return false;
}
});
}
/**
* Handles all {@link MobileWallet} API requests and determines the correct MWA RPC method and params to call.
* This handles backwards compatibility, based on the provided @protocolVersion.
*
* @param methodName the name of {@link MobileWallet} method that was called
* @param methodParams the parameters that were passed to the method
* @param protocolVersion the protocol version in use for this session/request
* @returns the RPC request method and params that should be sent to the wallet endpoint
*/
function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
let params = methodParams;
let method = methodName.toString().replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).toLowerCase();
switch (methodName) {
case "authorize": {
const authorizeParams = params;
let { chain } = authorizeParams;
if (protocolVersion === "legacy") {
switch (chain) {
case "solana:testnet":
chain = "testnet";
break;
case "solana:devnet":
chain = "devnet";
break;
case "solana:mainnet":
chain = "mainnet-beta";
break;
default: chain = authorizeParams.cluster;
}
authorizeParams.cluster = chain;
params = authorizeParams;
} else {
switch (chain) {
case "testnet":
case "devnet":
chain = `solana:${chain}`;
break;
case "mainnet-beta":
chain = "solana:mainnet";
break;
}
authorizeParams.chain = chain;
params = authorizeParams;
}
}
case "reauthorize": {
const { auth_token, identity } = params;
if (auth_token) switch (protocolVersion) {
case "legacy":
method = "reauthorize";
params = {
auth_token,
identity
};
break;
default:
method = "authorize";
break;
}
break;
}
}
return {
method,
params
};
}
/**
* Handles all {@link MobileWallet} API responses and modifies the response for backwards compatibility, if needed
*
* @param method the {@link MobileWallet} method that was called
* @param response the original response that was returned by the method call
* @param protocolVersion the protocol version in use for this session/request
* @returns the possibly modified response
*/
function handleMobileWalletResponse(method, response, protocolVersion) {
switch (method) {
case "getCapabilities": {
const capabilities = response;
switch (protocolVersion) {
case "legacy": {
const features = [SolanaSignTransactions];
if (capabilities.supports_clone_authorization === true) features.push(SolanaCloneAuthorization);
return {
...capabilities,
features
};
}
case "v1": return {
...capabilities,
supports_sign_and_send_transactions: true,
supports_clone_authorization: capabilities.features.includes(SolanaCloneAuthorization)
};
}
}
}
return response;
}
async function signInFallback(signInPayload, authorizationResult, protocolRequestHandler) {
const domain = signInPayload.domain ?? window.location.host;
const address = authorizationResult.accounts[0].address;
const siwsMessage = createSIWSMessageBase64Url({
...signInPayload,
domain,
address: base64ToBase58(address)
});
const signedPayload = toUint8Array((await protocolRequestHandler("sign_messages", {
addresses: [address],
payloads: [siwsMessage]
})).signed_payloads[0]);
const signedMessage = fromUint8Array$1(signedPayload.slice(0, signedPayload.length - 64));
const signature = fromUint8Array$1(signedPayload.slice(signedPayload.length - 64));
return {
address,
signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
signature
};
}
function createSequenceNumberVector(sequenceNumber) {
if (sequenceNumber >= 4294967296) throw new Error("Outbound sequence number overflow. The maximum sequence number is 32-bytes.");
const byteArray = /* @__PURE__ */ new ArrayBuffer(4);
new DataView(byteArray).setUint32(0, sequenceNumber, false);
return new Uint8Array(byteArray);
}
//#endregion
//#region src/encryptedMessage.ts
const INITIALIZATION_VECTOR_BYTES = 12;
async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
const sequenceNumberVector = createSequenceNumberVector(sequenceNumber);
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
crypto.getRandomValues(initializationVector);
const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, new TextEncoder().encode(plaintext));
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
response.set(new Uint8Array(sequenceNumberVector), 0);
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
response.set(new Uint8Array(ciphertext), sequenceNumberVector.byteLength + initializationVector.byteLength);
return response;
}
async function decryptMessage(message, sharedSecret) {
const sequenceNumberVector = message.slice(0, 4);
const initializationVector = message.slice(4, 4 + INITIALIZATION_VECTOR_BYTES);
const ciphertext = message.slice(4 + INITIALIZATION_VECTOR_BYTES);
const plaintextBuffer = await crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
return getUtf8Decoder().decode(plaintextBuffer);
}
function getAlgorithmParams(sequenceNumber, initializationVector) {
return {
additionalData: sequenceNumber,
iv: initializationVector,
name: "AES-GCM",
tagLength: 128
};
}
let _utf8Decoder;
function getUtf8Decoder() {
if (_utf8Decoder === void 0) _utf8Decoder = new TextDecoder("utf-8");
return _utf8Decoder;
}
//#endregion
//#region src/generateAssociationKeypair.ts
async function generateAssociationKeypair() {
return await crypto.subtle.generateKey({
name: "ECDSA",
namedCurve: "P-256"
}, false, ["sign"]);
}
//#endregion
//#region src/generateECDHKeypair.ts
async function generateECDHKeypair() {
return await crypto.subtle.generateKey({
name: "ECDH",
namedCurve: "P-256"
}, false, ["deriveKey", "deriveBits"]);
}
//#endregion
//#region src/arrayBufferToBase64String.ts
function arrayBufferToBase64String(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let ii = 0; ii < len; ii++) binary += String.fromCharCode(bytes[ii]);
return window.btoa(binary);
}
//#endregion
//#region src/associationPort.ts
function getRandomAssociationPort() {
return assertAssociationPort(49152 + Math.floor(Math.random() * 16384));
}
function assertAssociationPort(port) {
if (port < 49152 || port > 65535) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${port} given.`, { port });
return port;
}
//#endregion
//#region src/getStringWithURLUnsafeBase64CharactersReplaced.ts
function getStringWithURLUnsafeCharactersReplaced(unsafeBase64EncodedString) {
return unsafeBase64EncodedString.replace(/[/+=]/g, (m) => ({
"/": "_",
"+": "-",
"=": "."
})[m]);
}
//#endregion
//#region src/getAssociateAndroidIntentURL.ts
const INTENT_NAME = "solana-wallet";
function getPathParts(pathString) {
return pathString.replace(/(^\/+|\/+$)/g, "").split("/");
}
function getIntentURL(methodPathname, intentUrlBase) {
let baseUrl = null;
if (intentUrlBase) {
try {
baseUrl = new URL(intentUrlBase);
} catch {}
if (baseUrl?.protocol !== "https:") throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, "Base URLs supplied by wallets must be valid `https` URLs");
}
baseUrl ||= new URL(`${INTENT_NAME}:/`);
const pathname = methodPathname.startsWith("/") ? methodPathname : [...getPathParts(baseUrl.pathname), ...getPathParts(methodPathname)].join("/");
return new URL(pathname, baseUrl);
}
async function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ["v1"]) {
const associationPort = assertAssociationPort(putativePort);
const encodedKey = arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
const url = getIntentURL("v1/associate/local", associationURLBase);
url.searchParams.set("association", getStringWithURLUnsafeCharactersReplaced(encodedKey));
url.searchParams.set("port", `${associationPort}`);
protocolVersions.forEach((version) => {
url.searchParams.set("v", version);
});
return url;
}
async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ["v1"]) {
const encodedKey = arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
const url = getIntentURL("v1/associate/remote", associationURLBase);
url.searchParams.set("association", getStringWithURLUnsafeCharactersReplaced(encodedKey));
url.searchParams.set("reflector", `${hostAuthority}`);
url.searchParams.set("id", `${fromUint8Array$1(reflectorId, true)}`);
protocolVersions.forEach((version) => {
url.searchParams.set("v", version);
});
return url;
}
//#endregion
//#region src/jsonRpcMessage.ts
async function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
const plaintext = JSON.stringify(jsonRpcMessage);
const sequenceNumber = jsonRpcMessage.id;
return encryptMessage(plaintext, sequenceNumber, sharedSecret);
}
async function decryptJsonRpcMessage(message, sharedSecret) {
const plaintext = await decryptMessage(message, sharedSecret);
const jsonRpcMessage = JSON.parse(plaintext);
if (Object.hasOwnProperty.call(jsonRpcMessage, "error")) throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
return jsonRpcMessage;
}
//#endregion
//#region src/parseHelloRsp.ts
async function parseHelloRsp(payloadBuffer, associationPublicKey, ecdhPrivateKey) {
const [associationPublicKeyBuffer, walletPublicKey] = await Promise.all([crypto.subtle.exportKey("raw", associationPublicKey), crypto.subtle.importKey("raw", payloadBuffer.slice(0, 65), {
name: "ECDH",
namedCurve: "P-256"
}, false, [])]);
const sharedSecret = await crypto.subtle.deriveBits({
name: "ECDH",
public: walletPublicKey
}, ecdhPrivateKey, 256);
const ecdhSecretKey = await crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"]);
return await crypto.subtle.deriveKey({
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(associationPublicKeyBuffer),
info: new Uint8Array()
}, ecdhSecretKey, {
name: "AES-GCM",
length: 128
}, false, ["encrypt", "decrypt"]);
}
//#endregion
//#region src/parseSessionProps.ts
async function parseSessionProps(message, sharedSecret) {
const plaintext = await decryptMessage(message, sharedSecret);
const jsonProperties = JSON.parse(plaintext);
let protocolVersion = "legacy";
if (Object.hasOwnProperty.call(jsonProperties, "v")) switch (jsonProperties.v) {
case 1:
case "1":
case "v1":
protocolVersion = "v1";
break;
case "legacy":
protocolVersion = "legacy";
break;
default: throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION, `Unknown/unsupported protocol version: ${jsonProperties.v}`);
}
return { protocol_version: protocolVersion };
}
//#endregion
//#region src/startSession.ts
const Browser = {
Firefox: 0,
Other: 1
};
function assertUnreachable(x) {
return x;
}
function getBrowser() {
return navigator.userAgent.indexOf("Firefox/") !== -1 ? Browser.Firefox : Browser.Other;
}
function getDetectionPromise() {
return new Promise((resolve, reject) => {
function cleanup() {
clearTimeout(timeoutId);
window.removeEventListener("blur", handleBlur);
}
function handleBlur() {
cleanup();
resolve();
}
window.addEventListener("blur", handleBlur);
const timeoutId = setTimeout(() => {
cleanup();
reject();
}, 3e3);
});
}
let _frame = null;
function launchUrlThroughHiddenFrame(url) {
if (_frame == null) {
_frame = document.createElement("iframe");
_frame.style.display = "none";
document.body.appendChild(_frame);
}
_frame.contentWindow.location.href = url.toString();
}
async function launchAssociation(associationUrl) {
if (associationUrl.protocol === "https:") window.location.assign(associationUrl);
else try {
const browser = getBrowser();
switch (browser) {
case Browser.Firefox:
launchUrlThroughHiddenFrame(associationUrl);
break;
case Browser.Other: {
const detectionPromise = getDetectionPromise();
window.location.assign(associationUrl);
await detectionPromise;
break;
}
default: assertUnreachable(browser);
}
} catch {
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, "Found no installed wallet that supports the mobile wallet protocol.");
}
}
async function startSession(associationPublicKey, associationURLBase) {
const randomAssociationPort = getRandomAssociationPort();
await launchAssociation(await getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase));
return randomAssociationPort;
}
//#endregion
//#region src/transact.ts
const WEBSOCKET_CONNECTION_CONFIG = {
retryDelayScheduleMs: [
150,
150,
200,
500,
500,
750,
750,
1e3
],
timeoutMs: 3e4
};
const WEBSOCKET_PROTOCOL_BINARY = "com.solana.mobilewalletadapter.v1";
const WEBSOCKET_PROTOCOL_BASE64 = "com.solana.mobilewalletadapter.v1.base64";
function assertSecureContext() {
if (typeof window === "undefined" || window.isSecureContext !== true) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SECURE_CONTEXT_REQUIRED, "The mobile wallet adapter protocol must be used in a secure context (`https`).");
}
function assertSecureEndpointSpecificURI(walletUriBase) {
let url;
try {
url = new URL(walletUriBase);
} catch {
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, "Invalid base URL supplied by wallet");
}
if (url.protocol !== "https:") throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, "Base URLs supplied by wallets must be valid `https` URLs");
}
function getSequenceNumberFromByteArray(byteArray) {
return new DataView(byteArray).getUint32(0, false);
}
function decodeVarLong(byteArray) {
const bytes = new Uint8Array(byteArray);
const l = byteArray.byteLength;
const limit = 10;
let value = 0, offset = 0, b;
do {
if (offset >= l || offset > limit) throw new RangeError("Failed to decode varint");
b = bytes[offset++];
value |= (b & 127) << 7 * offset;
} while (b >= 128);
return {
value,
offset
};
}
function getReflectorIdFromByteArray(byteArray) {
const { value: length, offset } = decodeVarLong(byteArray);
return new Uint8Array(byteArray.slice(offset, offset + length));
}
async function transact(callback, config) {
const { wallet, close } = await startScenario(config);
try {
return await callback(await wallet);
} finally {
close();
}
}
async function startScenario(config) {
assertSecureContext();
const associationKeypair = await generateAssociationKeypair();
const websocketURL = `ws://localhost:${await startSession(associationKeypair.publicKey, config?.baseUri)}/solana-wallet`;
let connectionStartTime;
const getNextRetryDelayMs = (() => {
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
return () => schedule.length > 1 ? schedule.shift() : schedule[0];
})();
let nextJsonRpcMessageId = 1;
let lastKnownInboundSequenceNumber = 0;
let state = { __type: "disconnected" };
let socket;
let sessionEstablished = false;
let handleForceClose;
return {
close: () => {
socket.close();
handleForceClose();
},
wallet: new Promise((resolve, reject) => {
const jsonRpcResponsePromises = {};
const handleOpen = async () => {
if (state.__type !== "connecting") {
console.warn(`Expected adapter state to be \`connecting\` at the moment the websocket opens. Got \`${state.__type}\`.`);
return;
}
socket.removeEventListener("open", handleOpen);
const { associationKeypair } = state;
const ecdhKeypair = await generateECDHKeypair();
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
};
const handleClose = (evt) => {
if (evt.wasClean) state = { __type: "disconnected" };
else reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
disposeSocket();
};
const handleError = async (_evt) => {
disposeSocket();
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
else {
await new Promise((resolve) => {
const retryDelayMs = getNextRetryDelayMs();
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
});
attemptSocketConnection();
}
};
const handleMessage = async (evt) => {
const responseBuffer = await evt.data.arrayBuffer();
switch (state.__type) {
case "connecting": {
if (responseBuffer.byteLength !== 0) throw new Error("Encountered unexpected message while connecting");
const ecdhKeypair = await generateECDHKeypair();
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
break;
}
case "connected":
try {
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
delete jsonRpcResponsePromises[jsonRpcMessage.id];
responsePromise.resolve(jsonRpcMessage.result);
} catch (e) {
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
responsePromise.reject(e);
} else throw e;
}
break;
case "hello_req_sent": {
if (responseBuffer.byteLength === 0) {
const ecdhKeypair = await generateECDHKeypair();
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
break;
}
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
const sessionPropertiesBuffer = responseBuffer.slice(65);
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
})() : { protocol_version: "legacy" };
state = {
__type: "connected",
sharedSecret,
sessionProperties
};
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
const id = nextJsonRpcMessageId++;
socket.send(await encryptJsonRpcMessage({
id,
jsonrpc: "2.0",
method,
params: params ?? {}
}, sharedSecret));
return new Promise((resolve, reject) => {
jsonRpcResponsePromises[id] = {
resolve(result) {
switch (method) {
case "authorize":
case "reauthorize": {
const { wallet_uri_base } = result;
if (wallet_uri_base != null) try {
assertSecureEndpointSpecificURI(wallet_uri_base);
} catch (e) {
reject(e);
return;
}
break;
}
}
resolve(result);
},
reject
};
});
});
sessionEstablished = true;
try {
resolve(wallet);
} catch (e) {
reject(e);
}
break;
}
}
};
handleForceClose = () => {
socket.removeEventListener("message", handleMessage);
disposeSocket();
if (!sessionEstablished) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent("socket was closed before connection") }));
};
let disposeSocket;
let retryWaitTimeoutId;
const attemptSocketConnection = () => {
if (disposeSocket) disposeSocket();
state = {
__type: "connecting",
associationKeypair
};
if (connectionStartTime === void 0) connectionStartTime = Date.now();
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL_BINARY]);
socket.addEventListener("open", handleOpen);
socket.addEventListener("close", handleClose);
socket.addEventListener("error", handleError);
socket.addEventListener("message", handleMessage);
disposeSocket = () => {
window.clearTimeout(retryWaitTimeoutId);
socket.removeEventListener("open", handleOpen);
socket.removeEventListener("close", handleClose);
socket.removeEventListener("error", handleError);
socket.removeEventListener("message", handleMessage);
};
};
attemptSocketConnection();
})
};
}
async function startRemoteScenario(config) {
assertSecureContext();
const associationKeypair = await generateAssociationKeypair();
const websocketURL = `wss://${config?.remoteHostAuthority}/reflect`;
let connectionStartTime;
const getNextRetryDelayMs = (() => {
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
return () => schedule.length > 1 ? schedule.shift() : schedule[0];
})();
let nextJsonRpcMessageId = 1;
let lastKnownInboundSequenceNumber = 0;
let encoding;
let state = { __type: "disconnected" };
let socket;
let disposeSocket;
const decodeBytes = async (evt) => {
if (encoding == "base64") return toUint8Array(await evt.data).buffer;
else return await evt.data.arrayBuffer();
};
const associationUrl = await new Promise((resolve, reject) => {
const handleOpen = async () => {
if (state.__type !== "connecting") {
console.warn(`Expected adapter state to be \`connecting\` at the moment the websocket opens. Got \`${state.__type}\`.`);
return;
}
if (socket.protocol.includes(WEBSOCKET_PROTOCOL_BASE64)) encoding = "base64";
else encoding = "binary";
socket.removeEventListener("open", handleOpen);
};
const handleClose = (evt) => {
if (evt.wasClean) state = { __type: "disconnected" };
else reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
disposeSocket();
};
const handleError = async (_evt) => {
disposeSocket();
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
else {
await new Promise((resolve) => {
const retryDelayMs = getNextRetryDelayMs();
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
});
attemptSocketConnection();
}
};
const handleReflectorIdMessage = async (evt) => {
const responseBuffer = await decodeBytes(evt);
if (state.__type === "connecting") {
if (responseBuffer.byteLength == 0) throw new Error("Encountered unexpected message while connecting");
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
state = {
__type: "reflector_id_received",
reflectorId
};
const associationUrl = await getRemoteAssociateAndroidIntentURL(associationKeypair.publicKey, config.remoteHostAuthority, reflectorId, config?.baseUri);
socket.removeEventListener("message", handleReflectorIdMessage);
resolve(associationUrl);
}
};
let retryWaitTimeoutId;
const attemptSocketConnection = () => {
if (disposeSocket) disposeSocket();
state = {
__type: "connecting",
associationKeypair
};
if (connectionStartTime === void 0) connectionStartTime = Date.now();
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL_BINARY, WEBSOCKET_PROTOCOL_BASE64]);
socket.addEventListener("open", handleOpen);
socket.addEventListener("close", handleClose);
socket.addEventListener("error", handleError);
socket.addEventListener("message", handleReflectorIdMessage);
disposeSocket = () => {
window.clearTimeout(retryWaitTimeoutId);
socket.removeEventListener("open", handleOpen);
socket.removeEventListener("close", handleClose);
socket.removeEventListener("error", handleError);
socket.removeEventListener("message", handleReflectorIdMessage);
};
};
attemptSocketConnection();
});
let sessionEstablished = false;
let handleClose;
return {
associationUrl,
close: () => {
socket.close();
handleClose();
},
wallet: new Promise((resolve, reject) => {
const jsonRpcResponsePromises = {};
const handleMessage = async (evt) => {
const responseBuffer = await decodeBytes(evt);
switch (state.__type) {
case "reflector_id_received": {
if (responseBuffer.byteLength !== 0) throw new Error("Encountered unexpected message while awaiting reflection");
const ecdhKeypair = await generateECDHKeypair();
const binaryMsg = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
if (encoding == "base64") socket.send(fromUint8Array$1(binaryMsg));
else socket.send(binaryMsg);
state = {
__type: "hello_req_sent",
associationPublicKey: associationKeypair.publicKey,
ecdhPrivateKey: ecdhKeypair.privateKey
};
break;
}
case "connected":
try {
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
delete jsonRpcResponsePromises[jsonRpcMessage.id];
responsePromise.resolve(jsonRpcMessage.result);
} catch (e) {
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
responsePromise.reject(e);
} else throw e;
}
break;
case "hello_req_sent": {
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
const sessionPropertiesBuffer = responseBuffer.slice(65);
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
lastKnownInboundSequenceNumber = sequenceNumber;
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
})() : { protocol_version: "legacy" };
state = {
__type: "connected",
sharedSecret,
sessionProperties
};
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
const id = nextJsonRpcMessageId++;
const binaryMsg = await encryptJsonRpcMessage({
id,
jsonrpc: "2.0",
method,
params: params ?? {}
}, sharedSecret);
if (encoding == "base64") socket.send(fromUint8Array$1(binaryMsg));
else socket.send(binaryMsg);
return new Promise((resolve, reject) => {
jsonRpcResponsePromises[id] = {
resolve(result) {
switch (method) {
case "authorize":
case "reauthorize": {
const { wallet_uri_base } = result;
if (wallet_uri_base != null) try {
assertSecureEndpointSpecificURI(wallet_uri_base);
} catch (e) {
reject(e);
return;
}
break;
}
}
resolve(result);
},
reject
};
});
});
sessionEstablished = true;
try {
resolve(wallet);
} catch (e) {
reject(e);
}
break;
}
}
};
socket.addEventListener("message", handleMessage);
handleClose = () => {
socket.removeEventListener("message", handleMessage);
disposeSocket();
if (!sessionEstablished) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent("socket was closed before connection") }));
};
})
};
}
//#endregion
export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, startRemoteScenario, startScenario, transact };
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"module"}