@oasisprotocol/sapphire-paratime
Version:
The Sapphire ParaTime Web3 integration library.
219 lines • 9.86 kB
JavaScript
;
// SPDX-License-Identifier: Apache-2.0
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeTaggedProxyObject = exports.makeSapphireRequestFn = exports.isCallDataPublicKeyQuery = exports.isWrappedRequestFn = exports.notifySapphireSnap = exports.detectSapphireSnap = exports.wrapEthereumProvider = exports.isWrappedEthereumProvider = exports.fillOptions = exports.isLegacyProvider = exports.isEthereumProvider = void 0;
const ethersutils_js_1 = require("./ethersutils.cjs");
const calldatapublickey_js_1 = require("./calldatapublickey.cjs");
const constants_js_1 = require("./constants.cjs");
function isEthereumProvider(p) {
return 'request' in p && typeof p.request === 'function';
}
exports.isEthereumProvider = isEthereumProvider;
function isLegacyProvider(p) {
return 'send' in p && typeof p.send === 'function';
}
exports.isLegacyProvider = isLegacyProvider;
function fillOptions(options) {
if (!options) {
options = {};
}
if (!options.fetcher) {
options.fetcher = new calldatapublickey_js_1.KeyFetcher();
}
return options;
}
exports.fillOptions = fillOptions;
// -----------------------------------------------------------------------------
// Wrap an Ethereum compatible provider to expose a consistent request() iface
const SAPPHIRE_WRAPPED_ETHEREUMPROVIDER = '#SAPPHIRE_WRAPPED_ETHEREUMPROVIDER';
function isWrappedEthereumProvider(p) {
return p && SAPPHIRE_WRAPPED_ETHEREUMPROVIDER in p;
}
exports.isWrappedEthereumProvider = isWrappedEthereumProvider;
/**
* Wrap an EIP-1193 or EIP-2696 compatible provider with Sapphire encryption
*
* ```typescript
* const provider = wrapEthereumProvider(window.ethereum);
* ```
*
* @param upstream Provides a send() or request() function
* @param options (optional) Re-use parameters from other providers
* @returns Sapphire wrapped provider
*/
function wrapEthereumProvider(upstream, options) {
if (isWrappedEthereumProvider(upstream)) {
return upstream;
}
if (!isEthereumProvider(upstream) && !isLegacyProvider(upstream)) {
throw new Error('It is neither an Ethereum nor a Legacy provider');
}
const filled_options = fillOptions(options);
// if upstream provides a send() function but not request function
// then derive a request() function from the send() function
// if we do this, don't then re-wrap the send() function
// only wrap the send() function if there was a request() function
const request = makeSapphireRequestFn(upstream, filled_options);
const hooks = { request };
// We prefer a request() method, but a provider may expose a send() method
// Like Hardhat's LazyInitializationProviderAdapter, which is used with Ethers
// So, everything gets sent through the Sapphire-wrapped request() function
if ('send' in upstream)
hooks.send = (method, params) => {
return request({ method, params });
};
// sendAsync implementations vary too widely to be used as a standard
if ('sendAsync' in upstream)
hooks.sendAsync = () => {
throw new Error('sendAsync not supported!');
};
return makeTaggedProxyObject(upstream, SAPPHIRE_WRAPPED_ETHEREUMPROVIDER, filled_options, hooks);
}
exports.wrapEthereumProvider = wrapEthereumProvider;
const SAPPHIRE_SNAP_ID = 'npm:@oasisprotocol/sapphire-snap';
function detectSapphireSnap(provider) {
return __awaiter(this, void 0, void 0, function* () {
try {
const installedSnaps = (yield provider.request({
method: 'wallet_getSnaps',
}));
for (const snap of Object.values(installedSnaps)) {
if (snap.id === SAPPHIRE_SNAP_ID) {
return snap.id;
}
}
}
catch (e) {
return undefined;
}
});
}
exports.detectSapphireSnap = detectSapphireSnap;
function notifySapphireSnap(snapId, cipher, transactionData, options, provider) {
return __awaiter(this, void 0, void 0, function* () {
if (cipher.ephemeralKey) {
const peerPublicKey = yield options.fetcher.fetch(provider);
yield provider.request({
method: 'wallet_invokeSnap',
params: {
snapId: snapId,
request: {
method: 'setTransactionDecryptKeys',
params: {
id: transactionData,
ephemeralSecretKey: (0, ethersutils_js_1.hexlify)(cipher.ephemeralKey),
peerPublicKey: (0, ethersutils_js_1.hexlify)(peerPublicKey.key),
peerPublicKeyEpoch: peerPublicKey.epoch,
},
},
},
});
}
});
}
exports.notifySapphireSnap = notifySapphireSnap;
const SAPPHIRE_EIP1193_REQUESTFN = '#SAPPHIRE_EIP1193_REQUESTFN';
function isWrappedRequestFn(p) {
return p && SAPPHIRE_EIP1193_REQUESTFN in p;
}
exports.isWrappedRequestFn = isWrappedRequestFn;
function isCallDataPublicKeyQuery(params) {
return (params &&
Array.isArray(params) &&
params.length > 0 &&
params[0].to === constants_js_1.SUBCALL_ADDR &&
params[0].data === constants_js_1.CALLDATAPUBLICKEY_CALLDATA);
}
exports.isCallDataPublicKeyQuery = isCallDataPublicKeyQuery;
/**
* Creates an EIP-1193 compatible request() function
* @param provider Upstream EIP-1193 provider to forward requests to
* @param options
* @returns
*/
function makeSapphireRequestFn(provider, options) {
if (isWrappedRequestFn(provider.request)) {
return provider.request;
}
const filled_options = fillOptions(options);
const f = (args) => __awaiter(this, void 0, void 0, function* () {
const snapId = filled_options.enableSapphireSnap
? yield detectSapphireSnap(provider)
: undefined;
const { method, params } = args;
let transactionData = undefined;
// Encrypt requests which can be encrypted
if (params &&
Array.isArray(params) &&
/^eth_((send|sign)Transaction|call|estimateGas)$/.test(method) &&
params[0].data // Ignore balance transfers without calldata
) {
// TODO: should we attempt to detect `if (not sapphire) throw` instead of
// failing to fetch public key in the next line?
const cipher = yield filled_options.fetcher.cipher(provider);
transactionData = params[0].data = cipher.encryptCall(params[0].data);
if (snapId !== undefined && transactionData !== undefined) {
// Run in background so as to not delay results
notifySapphireSnap(snapId, cipher, transactionData, filled_options, provider);
}
const res = yield provider.request({
method,
params: params !== null && params !== void 0 ? params : [],
});
// Decrypt responses which return encrypted data
if (method === 'eth_call') {
// If it's an unencrypted core.CallDataPublicKey query, don't attempt to decrypt the response
if (!isCallDataPublicKeyQuery(params)) {
return cipher.decryptResult(res);
}
}
return res;
}
else {
const res = yield provider.request({
method,
params: params !== null && params !== void 0 ? params : [],
});
return res;
}
});
return makeTaggedProxyObject(f, SAPPHIRE_EIP1193_REQUESTFN, filled_options);
}
exports.makeSapphireRequestFn = makeSapphireRequestFn;
// -----------------------------------------------------------------------------
function makeTaggedProxyObject(upstream, propname, options, hooks) {
return new Proxy(upstream, {
has(target, p) {
if (p === propname)
return true;
return Reflect.has(target, p);
},
get(upstream, prop) {
var _a;
if (prop === propname)
return options;
if (hooks && prop in hooks)
return Reflect.get(hooks, prop);
const value = Reflect.get(upstream, prop);
// Brave wallet web3provider properties are read only and throw typeerror
// https://github.com/brave/brave-core/blob/74bf470a0291ea3719f1a75af066ee10b7057dbd/components/brave_wallet/resources/ethereum_provider.js#L13-L27
// https://github.com/wevm/wagmi/blob/86c42248c2f34260a52ee85183c607315ae63ce8/packages/core/src/connectors/injected.ts#L327-L335
const propWritable = ((_a = Object.getOwnPropertyDescriptor(upstream, prop)) === null || _a === void 0 ? void 0 : _a.writable) !== false;
if (typeof value === 'function' && propWritable) {
return value.bind(upstream);
}
return value;
},
});
}
exports.makeTaggedProxyObject = makeTaggedProxyObject;
//# sourceMappingURL=provider.js.map