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,924 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let _solana_codecs_strings = require("@solana/codecs-strings");
let _solana_wallet_standard_util = require("@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 (0, _solana_codecs_strings.getBase58Decoder)().decode(byteArray);
}
function base64ToBase58(base64EncodedString) {
return fromUint8Array(toUint8Array(base64EncodedString));
}
//#endregion
//#region src/createSIWSMessage.ts
function createSIWSMessage(payload) {
return (0, _solana_wallet_standard_util.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
exports.SolanaCloneAuthorization = SolanaCloneAuthorization;
exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
exports.SolanaMobileWalletAdapterProtocolError = SolanaMobileWalletAdapterProtocolError;
exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
exports.SolanaSignInWithSolana = SolanaSignInWithSolana;
exports.SolanaSignTransactions = SolanaSignTransactions;
exports.startRemoteScenario = startRemoteScenario;
exports.startScenario = startScenario;
exports.transact = transact;
//# sourceMappingURL=index.browser.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,924 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let _solana_codecs_strings = require("@solana/codecs-strings");
let _solana_wallet_standard_util = require("@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 (0, _solana_codecs_strings.getBase58Decoder)().decode(byteArray);
}
function base64ToBase58(base64EncodedString) {
return fromUint8Array(toUint8Array(base64EncodedString));
}
//#endregion
//#region src/createSIWSMessage.ts
function createSIWSMessage(payload) {
return (0, _solana_wallet_standard_util.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
exports.SolanaCloneAuthorization = SolanaCloneAuthorization;
exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
exports.SolanaMobileWalletAdapterProtocolError = SolanaMobileWalletAdapterProtocolError;
exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
exports.SolanaSignInWithSolana = SolanaSignInWithSolana;
exports.SolanaSignTransactions = SolanaSignTransactions;
exports.startRemoteScenario = startRemoteScenario;
exports.startScenario = startScenario;
exports.transact = transact;
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,280 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let react_native = require("react-native");
let _solana_codecs_strings = require("@solana/codecs-strings");
let js_base64 = require("js-base64");
let _solana_wallet_standard_util = require("@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/codegenSpec/NativeSolanaMobileWalletAdapter.ts
var NativeSolanaMobileWalletAdapter_default = react_native.TurboModuleRegistry.getEnforcing("SolanaMobileWalletAdapter");
//#endregion
//#region src/base58Utils.ts
function fromUint8Array(byteArray) {
return (0, _solana_codecs_strings.getBase58Decoder)().decode(byteArray);
}
function base64ToBase58(base64EncodedString) {
return fromUint8Array((0, js_base64.toUint8Array)(base64EncodedString));
}
//#endregion
//#region src/createSIWSMessage.ts
function createSIWSMessage(payload) {
return (0, _solana_wallet_standard_util.createSignInMessageText)(payload);
}
function createSIWSMessageBase64Url(payload) {
return (0, js_base64.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 = (0, js_base64.toUint8Array)((await protocolRequestHandler("sign_messages", {
addresses: [address],
payloads: [siwsMessage]
})).signed_payloads[0]);
const signedMessage = (0, js_base64.fromUint8Array)(signedPayload.slice(0, signedPayload.length - 64));
const signature = (0, js_base64.fromUint8Array)(signedPayload.slice(signedPayload.length - 64));
return {
address,
signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
signature
};
}
//#endregion
//#region src/__forks__/react-native/transact.ts
react_native.AppRegistry.registerHeadlessTask("SolanaMobileWalletAdapterSessionBackgroundTask", () => {
return async () => {};
});
const LINKING_ERROR = "The package 'solana-mobile-wallet-adapter-protocol' doesn't seem to be linked. Make sure: \n\n- You rebuilt the app after installing the package\n- If you are using Lerna workspaces\n - You have added `@solana-mobile/mobile-wallet-adapter-protocol` as an explicit dependency, and\n - You have added `@solana-mobile/mobile-wallet-adapter-protocol` to the `nohoist` section of your package.json\n- You are not using Expo managed workflow\n";
const SolanaMobileWalletAdapter = react_native.Platform.OS === "android" && NativeSolanaMobileWalletAdapter_default ? NativeSolanaMobileWalletAdapter_default : new Proxy({}, { get() {
throw new Error(react_native.Platform.OS !== "android" ? "The package `solana-mobile-wallet-adapter-protocol` is only compatible with React Native Android" : LINKING_ERROR);
} });
function getErrorMessage(e) {
switch (e.code) {
case "ERROR_WALLET_NOT_FOUND": return "Found no installed wallet that supports the mobile wallet protocol.";
default: return e.message;
}
}
function handleError(e) {
if (e instanceof Error) {
const reactNativeError = e;
switch (reactNativeError.code) {
case void 0: throw e;
case "JSON_RPC_ERROR": {
const details = reactNativeError.userInfo;
throw new SolanaMobileWalletAdapterProtocolError(0, details.jsonRpcErrorCode, e.message);
}
default: throw new SolanaMobileWalletAdapterError(reactNativeError.code, getErrorMessage(reactNativeError), reactNativeError.userInfo);
}
}
throw e;
}
async function transact(callback, config) {
let didSuccessfullyConnect = false;
try {
const sessionProperties = await SolanaMobileWalletAdapter.startSession(config);
didSuccessfullyConnect = true;
return await callback(createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
try {
return SolanaMobileWalletAdapter.invoke(method, params);
} catch (e) {
return handleError(e);
}
}));
} catch (e) {
return handleError(e);
} finally {
if (didSuccessfullyConnect) await SolanaMobileWalletAdapter.endSession();
}
}
//#endregion
exports.SolanaCloneAuthorization = SolanaCloneAuthorization;
exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
exports.SolanaMobileWalletAdapterProtocolError = SolanaMobileWalletAdapterProtocolError;
exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
exports.SolanaSignInWithSolana = SolanaSignInWithSolana;
exports.SolanaSignTransactions = SolanaSignTransactions;
exports.transact = transact;
//# sourceMappingURL=index.native.js.map

File diff suppressed because one or more lines are too long

View File

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

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"}

View File

@@ -0,0 +1,261 @@
import { SolanaSignInInput } from "@solana/wallet-standard-features";
import { IdentifierArray, IdentifierString, WalletAccount, WalletIcon } from "@wallet-standard/core";
//#region src/errors.d.ts
declare const SolanaMobileWalletAdapterErrorCode: {
readonly ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: "ERROR_ASSOCIATION_PORT_OUT_OF_RANGE";
readonly ERROR_REFLECTOR_ID_OUT_OF_RANGE: "ERROR_REFLECTOR_ID_OUT_OF_RANGE";
readonly ERROR_FORBIDDEN_WALLET_BASE_URL: "ERROR_FORBIDDEN_WALLET_BASE_URL";
readonly ERROR_SECURE_CONTEXT_REQUIRED: "ERROR_SECURE_CONTEXT_REQUIRED";
readonly ERROR_SESSION_CLOSED: "ERROR_SESSION_CLOSED";
readonly ERROR_SESSION_TIMEOUT: "ERROR_SESSION_TIMEOUT";
readonly ERROR_WALLET_NOT_FOUND: "ERROR_WALLET_NOT_FOUND";
readonly ERROR_INVALID_PROTOCOL_VERSION: "ERROR_INVALID_PROTOCOL_VERSION";
readonly ERROR_BROWSER_NOT_SUPPORTED: "ERROR_BROWSER_NOT_SUPPORTED";
readonly ERROR_LOOPBACK_ACCESS_BLOCKED: "ERROR_LOOPBACK_ACCESS_BLOCKED";
readonly ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED";
};
type SolanaMobileWalletAdapterErrorCodeEnum = (typeof SolanaMobileWalletAdapterErrorCode)[keyof typeof SolanaMobileWalletAdapterErrorCode];
type ErrorDataTypeMap = {
[SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE]: {
port: number;
};
[SolanaMobileWalletAdapterErrorCode.ERROR_REFLECTOR_ID_OUT_OF_RANGE]: {
id: number;
};
[SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_SECURE_CONTEXT_REQUIRED]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED]: {
closeEvent: CloseEvent;
};
[SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_BROWSER_NOT_SUPPORTED]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_LOOPBACK_ACCESS_BLOCKED]: undefined;
[SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_CANCELLED]: {
event: Event | undefined;
};
};
declare class SolanaMobileWalletAdapterError<TErrorCode extends SolanaMobileWalletAdapterErrorCodeEnum> extends Error {
data: ErrorDataTypeMap[TErrorCode] | undefined;
code: TErrorCode;
constructor(...args: ErrorDataTypeMap[TErrorCode] extends Record<string, unknown> ? [code: TErrorCode, message: string, data: ErrorDataTypeMap[TErrorCode]] : [code: TErrorCode, message: string]);
}
type JSONRPCErrorCode = number;
declare const SolanaMobileWalletAdapterProtocolErrorCode: {
readonly ERROR_AUTHORIZATION_FAILED: -1;
readonly ERROR_INVALID_PAYLOADS: -2;
readonly ERROR_NOT_SIGNED: -3;
readonly ERROR_NOT_SUBMITTED: -4;
readonly ERROR_TOO_MANY_PAYLOADS: -5;
readonly ERROR_ATTEST_ORIGIN_ANDROID: -100;
};
type SolanaMobileWalletAdapterProtocolErrorCodeEnum = (typeof SolanaMobileWalletAdapterProtocolErrorCode)[keyof typeof SolanaMobileWalletAdapterProtocolErrorCode];
type ProtocolErrorDataTypeMap = {
[SolanaMobileWalletAdapterProtocolErrorCode.ERROR_AUTHORIZATION_FAILED]: undefined;
[SolanaMobileWalletAdapterProtocolErrorCode.ERROR_INVALID_PAYLOADS]: undefined;
[SolanaMobileWalletAdapterProtocolErrorCode.ERROR_NOT_SIGNED]: undefined;
[SolanaMobileWalletAdapterProtocolErrorCode.ERROR_NOT_SUBMITTED]: undefined;
[SolanaMobileWalletAdapterProtocolErrorCode.ERROR_TOO_MANY_PAYLOADS]: undefined;
[SolanaMobileWalletAdapterProtocolErrorCode.ERROR_ATTEST_ORIGIN_ANDROID]: {
attest_origin_uri: string;
challenge: string;
context: string;
};
};
declare class SolanaMobileWalletAdapterProtocolError<TErrorCode extends SolanaMobileWalletAdapterProtocolErrorCodeEnum> extends Error {
data: ProtocolErrorDataTypeMap[TErrorCode] | undefined;
code: TErrorCode | JSONRPCErrorCode;
jsonRpcMessageId: number;
constructor(...args: ProtocolErrorDataTypeMap[TErrorCode] extends Record<string, unknown> ? [jsonRpcMessageId: number, code: TErrorCode | JSONRPCErrorCode, message: string, data: ProtocolErrorDataTypeMap[TErrorCode]] : [jsonRpcMessageId: number, code: TErrorCode | JSONRPCErrorCode, message: string]);
}
//#endregion
//#region ../../node_modules/.pnpm/@solana+web3.js@1.98.4_bufferutil@4.1.0_typescript@5.6.3_utf-8-validate@5.0.10/node_modules/@solana/web3.js/lib/index.d.ts
type TransactionVersion = 'legacy' | 0;
/**
* Versioned transaction class
*/
//#endregion
//#region src/types.d.ts
type Account = Readonly<{
address: Base64EncodedAddress;
label?: string;
icon?: WalletIcon;
chains?: IdentifierArray;
features?: IdentifierArray;
}> | WalletAccount;
/**
* Properties that wallets may present to users when an app
* asks for authorization to execute privileged methods (see
* {@link PrivilegedMethods}).
*/
type AppIdentity = Readonly<{
uri?: string;
icon?: string;
name?: string;
}>;
/**
* An ephemeral elliptic-curve keypair on the P-256 curve.
* This public key is used to create the association token.
* The private key is used during session establishment.
*/
type AssociationKeypair = CryptoKeyPair;
type ProtocolVersion = 'v1' | 'legacy';
type SessionProperties = Readonly<{
protocol_version: ProtocolVersion;
}>;
/**
* The context returned from a wallet after having authorized a given
* account for use with a given application. You can cache this and
* use it later to invoke privileged methods.
*/
type AuthorizationResult = Readonly<{
accounts: Account[];
auth_token: AuthToken;
wallet_uri_base: string;
sign_in_result?: SignInResult;
}>;
type AuthToken = string;
type Base64EncodedAddress = string;
type Base64EncodedSignature = string;
type Base64EncodedMessage = string;
type Base64EncodedSignedMessage = string;
type Base64EncodedSignedTransaction = string;
type Base64EncodedTransaction = string;
/**
* @deprecated Replaced by the 'chain' parameter, which adds multi-chain capability as per MWA 2.0 spec.
*/
type Cluster = 'devnet' | 'testnet' | 'mainnet-beta';
type Chain = IdentifierString | Cluster;
type Finality = 'confirmed' | 'finalized' | 'processed';
type WalletAssociationConfig = Readonly<{
baseUri?: string;
}>;
type RemoteWalletAssociationConfig = WalletAssociationConfig & Readonly<{
remoteHostAuthority: string;
}>;
interface AuthorizeAPI {
/**
* @deprecated Replaced by updated authorize() method, which adds MWA 2.0 spec support.
*/
authorize(params: {
cluster: Cluster;
identity: AppIdentity;
}): Promise<AuthorizationResult>;
authorize(params: {
identity: AppIdentity;
chain?: Chain;
features?: IdentifierArray;
addresses?: string[];
auth_token?: AuthToken;
sign_in_payload?: SignInPayload;
}): Promise<AuthorizationResult>;
}
interface CloneAuthorizationAPI {
cloneAuthorization(params: {
auth_token: AuthToken;
}): Promise<Readonly<{
auth_token: AuthToken;
}>>;
}
interface DeauthorizeAPI {
deauthorize(params: {
auth_token: AuthToken;
}): Promise<Readonly<Record<string, never>>>;
}
interface GetCapabilitiesAPI {
getCapabilities(): Promise<Readonly<{
max_transactions_per_request: number;
max_messages_per_request: number;
supported_transaction_versions: ReadonlyArray<TransactionVersion>;
features: IdentifierArray;
/**
* @deprecated Replaced by features array.
*/
supports_clone_authorization: boolean;
/**
* @deprecated Replaced by features array.
*/
supports_sign_and_send_transactions: boolean;
}>>;
}
interface ReauthorizeAPI {
reauthorize(params: {
auth_token: AuthToken;
identity: AppIdentity;
}): Promise<AuthorizationResult>;
}
interface SignMessagesAPI {
signMessages(params: {
addresses: Base64EncodedAddress[];
payloads: Base64EncodedMessage[];
}): Promise<Readonly<{
signed_payloads: Base64EncodedSignedMessage[];
}>>;
}
interface SignTransactionsAPI {
signTransactions(params: {
payloads: Base64EncodedTransaction[];
}): Promise<Readonly<{
signed_payloads: Base64EncodedSignedTransaction[];
}>>;
}
interface SignAndSendTransactionsAPI {
signAndSendTransactions(params: {
options?: Readonly<{
min_context_slot?: number;
commitment?: string;
skip_preflight?: boolean;
max_retries?: number;
wait_for_commitment_to_send_next_transaction?: boolean;
}>;
payloads: Base64EncodedTransaction[];
}): Promise<Readonly<{
signatures: Base64EncodedSignature[];
}>>;
}
interface MobileWallet extends AuthorizeAPI, CloneAuthorizationAPI, DeauthorizeAPI, GetCapabilitiesAPI, ReauthorizeAPI, SignMessagesAPI, SignTransactionsAPI, SignAndSendTransactionsAPI {}
interface TerminateSessionAPI {
terminateSession(): void;
}
interface RemoteMobileWallet extends MobileWallet, TerminateSessionAPI {}
declare const SolanaSignTransactions = "solana:signTransactions";
declare const SolanaCloneAuthorization = "solana:cloneAuthorization";
declare const SolanaSignInWithSolana = "solana:signInWithSolana";
type SignInPayload = Readonly<{
domain?: string;
address?: string;
statement?: string;
uri?: string;
version?: string;
chainId?: string;
nonce?: string;
issuedAt?: string;
expirationTime?: string;
notBefore?: string;
requestId?: string;
resources?: readonly string[];
}> | SolanaSignInInput;
type SignInPayloadWithRequiredFields = Partial<SignInPayload> & Required<Pick<SignInPayload, 'domain' | 'address'>>;
type SignInResult = Readonly<{
address: Base64EncodedAddress;
signed_message: Base64EncodedSignedMessage;
signature: Base64EncodedSignature;
signature_type?: string;
}>;
type Scenario = Readonly<{
wallet: Promise<MobileWallet>;
close: () => void;
}>;
type RemoteScenario = Scenario & Readonly<{
associationUrl: URL;
}>;
//#endregion
//#region src/transact.d.ts
declare function transact<TReturn>(callback: (wallet: MobileWallet) => TReturn, config?: WalletAssociationConfig): Promise<TReturn>;
declare function startScenario(config?: WalletAssociationConfig): Promise<Scenario>;
declare function startRemoteScenario(config: RemoteWalletAssociationConfig): Promise<RemoteScenario>;
//#endregion
export { Account, AppIdentity, AssociationKeypair, AuthToken, AuthorizationResult, AuthorizeAPI, Base64EncodedAddress, Base64EncodedTransaction, Chain, CloneAuthorizationAPI, type Cluster, DeauthorizeAPI, type Finality, GetCapabilitiesAPI, MobileWallet, ProtocolVersion, ReauthorizeAPI, RemoteMobileWallet, RemoteScenario, RemoteWalletAssociationConfig, Scenario, SessionProperties, SignAndSendTransactionsAPI, SignInPayload, SignInPayloadWithRequiredFields, SignInResult, SignMessagesAPI, SignTransactionsAPI, SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, TerminateSessionAPI, WalletAssociationConfig, startRemoteScenario, startScenario, transact };
//# sourceMappingURL=index.d.ts.map

File diff suppressed because one or more lines are too long