UNPKG

@sphereon/ssi-sdk-web3.headless-provider

Version:

652 lines (641 loc) • 21.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from2, except, desc) => { if (from2 && typeof from2 === "object" || typeof from2 === "function") { for (let key of __getOwnPropNames(from2)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { EthersHeadlessProvider: () => EthersHeadlessProvider, EthersKMSSigner: () => EthersKMSSigner, EthersKMSSignerBuilder: () => EthersKMSSignerBuilder, EventEmitter: () => EventEmitter, Web3Method: () => Web3Method, createRpcServer: () => createRpcServer, createService: () => createService, createServiceMethod: () => createServiceMethod, createWeb3Provider: () => createWeb3Provider, getAddressFromAgent: () => getAddressFromAgent, getKey: () => getKey, without: () => without }); module.exports = __toCommonJS(index_exports); // src/types.ts var Web3Method = /* @__PURE__ */ (function(Web3Method2) { Web3Method2["RequestAccounts"] = "eth_requestAccounts"; Web3Method2["Accounts"] = "eth_accounts"; Web3Method2["SendTransaction"] = "eth_sendTransaction"; Web3Method2["SwitchEthereumChain"] = "wallet_switchEthereumChain"; Web3Method2["AddEthereumChain"] = "wallet_addEthereumChain"; Web3Method2["SignMessage"] = "personal_sign"; Web3Method2["SignTypedData"] = "eth_signTypedData"; Web3Method2["SignTypedDataV1"] = "eth_signTypedData_v1"; Web3Method2["SignTypedDataV3"] = "eth_signTypedData_v3"; Web3Method2["SignTypedDataV4"] = "eth_signTypedData_v4"; return Web3Method2; })({}); function without(list, item) { const idx = list.indexOf(item); if (idx >= 0) { return list.slice(0, idx).concat(list.slice(idx + 1)); } return list; } __name(without, "without"); // src/ethers-headless-provider.ts var import_strings = require("@ethersproject/strings"); var import_eth_sig_util = require("@metamask/eth-sig-util"); var import_strict = __toESM(require("assert/strict"), 1); var import_ethers = require("ethers"); var import_rxjs = require("rxjs"); // src/errors.ts var ErrorWithCode = class extends Error { static { __name(this, "ErrorWithCode"); } code; constructor(message, code) { super(message), this.code = code; return this; } }; var Deny = /* @__PURE__ */ __name(() => new ErrorWithCode("The user rejected the request.", 4001), "Deny"); var Unauthorized = /* @__PURE__ */ __name(() => new ErrorWithCode("The requested method and/or account has not been authorized by the user.", 4100), "Unauthorized"); var Disconnected = /* @__PURE__ */ __name(() => new ErrorWithCode("The Provider is disconnected from all chains.", 4900), "Disconnected"); var UnrecognizedChainID = /* @__PURE__ */ __name(() => new ErrorWithCode("Unrecognized chain ID. Try adding the chain using `wallet_addEthereumChain` first.", 4902), "UnrecognizedChainID"); // src/event-emitter.ts var EventEmitter = class { static { __name(this, "EventEmitter"); } listeners = /* @__PURE__ */ Object.create(null); emit(eventName, ...args) { this.listeners[eventName]?.forEach((listener) => { listener(...args); }); return true; } on(eventName, listener) { this.listeners[eventName] ??= []; this.listeners[eventName]?.push(listener); return this; } off(eventName, listener) { const listeners = this.listeners[eventName] ?? []; for (const [i, listener_] of listeners.entries()) { if (listener === listener_) { listeners.splice(i, 1); break; } } return this; } once(eventName, listener) { const cb = /* @__PURE__ */ __name((...args) => { this.off(eventName, cb); listener(...args); }, "cb"); return this.on(eventName, cb); } }; // src/ethers-headless-provider.ts var EthersHeadlessProvider = class extends EventEmitter { static { __name(this, "EthersHeadlessProvider"); } chains; _pendingRequests = new import_rxjs.BehaviorSubject([]); _signers = []; _activeChainId; _rpc = {}; _config; _authorizedRequests = {}; constructor(signers, chains, config = {}) { super(), this.chains = chains; this._signers = signers; this._activeChainId = chains[0].chainId; this._config = Object.assign({ debug: true, logger: console.log }, config); } async request({ method, params }) { if (this._config.debug) { this._config.logger(JSON.stringify({ method, params })); } switch (method) { case "eth_call": case "eth_getBalance": case "eth_estimateGas": case "eth_blockNumber": case "eth_getBlockByNumber": case "eth_getTransactionByHash": case "eth_getTransactionReceipt": case "eth_feeHistory": return this.getRpc().send(method, params); case "eth_requestAccounts": case "eth_accounts": return this.waitAuthorization({ method, params }, async () => { const { chainId } = this.getCurrentChain(); this.emit("connect", { chainId }); return Promise.all(this._signers.map((wallet) => wallet.getAddress())); }, true, "eth_requestAccounts"); case "eth_chainId": { const { chainId } = this.getCurrentChain(); return "0x" + chainId.toString(16); } case "net_version": { const { chainId } = this.getCurrentChain(); return "" + chainId; } case "eth_sendTransaction": { return this.waitAuthorization({ method, params }, async () => { const wallet = this.getCurrentWallet(); const rpc = this.getRpc(); const { gas, from: from2, ...txRequest } = params[0]; const tx = await wallet.connect(rpc).sendTransaction(txRequest); return tx.hash; }); } case "eth_sign": { return this.waitAuthorization({ method, params }, async () => { const wallet = this.getCurrentWallet(); const rpc = this.getRpc(); const message = params[1]; return await wallet.connect(rpc).signMessage(message); }); } case "wallet_addEthereumChain": { return this.waitAuthorization({ method, params }, async () => { const chainId = Number(params[0].chainId); const { rpcUrl } = params[0]; this.addNetwork(chainId, rpcUrl); return null; }); } case "wallet_switchEthereumChain": { if (this._activeChainId === Number(params[0].chainId)) { return null; } return this.waitAuthorization({ method, params }, async () => { const chainId = Number(params[0].chainId); this.switchNetwork(chainId); return null; }); } case "personal_sign": { return this.waitAuthorization({ method, params }, async () => { const wallet = this.getCurrentWallet(); const address = await wallet.getAddress(); import_strict.default.equal(address, import_ethers.ethers.utils.getAddress(params[1])); const message = (0, import_strings.toUtf8String)(params[0]); const signature = await wallet.signMessage(message); if (this._config.debug) { this._config.logger("personal_sign", { message, signature }); } return signature; }); } case "eth_signTypedData": case "eth_signTypedData_v1": { return this.waitAuthorization({ method, params }, async () => { const wallet = this.getCurrentWallet(); const address = await wallet.getAddress(); import_strict.default.equal(address, import_ethers.ethers.utils.getAddress(params[1])); const msgParams = params[0]; return (0, import_eth_sig_util.signTypedData)({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), data: msgParams, version: import_eth_sig_util.SignTypedDataVersion.V1 }); }); } case "eth_signTypedData_v3": case "eth_signTypedData_v4": { return this.waitAuthorization({ method, params }, async () => { const wallet = this.getCurrentWallet(); const address = await wallet.getAddress(); import_strict.default.equal(address, import_ethers.ethers.utils.getAddress(params[0])); const msgParams = JSON.parse(params[1]); return (0, import_eth_sig_util.signTypedData)({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), data: msgParams, version: method === "eth_signTypedData_v4" ? import_eth_sig_util.SignTypedDataVersion.V4 : import_eth_sig_util.SignTypedDataVersion.V3 }); }); } default: return this.getRpc().send(method, params); } } getCurrentWallet() { const wallet = this._signers[0]; if (!wallet) { throw Unauthorized(); } return wallet; } waitAuthorization(requestInfo, task, permanentPermission = false, methodOverride) { const method = methodOverride ?? requestInfo.method; if (this._authorizedRequests[method]) { return task(); } return new Promise((resolve, reject) => { const pendingRequest = { requestInfo, authorize: /* @__PURE__ */ __name(async () => { if (permanentPermission) { this._authorizedRequests[method] = true; } resolve(await task()); }, "authorize"), reject(err) { reject(err); } }; this._pendingRequests.next(this._pendingRequests.getValue().concat(pendingRequest)); }); } consumeRequest(requestKind) { return (0, import_rxjs.firstValueFrom)(this._pendingRequests.pipe((0, import_rxjs.switchMap)((a) => (0, import_rxjs.from)(a)), (0, import_rxjs.filter)((request) => { return request.requestInfo.method === requestKind; }), (0, import_rxjs.first)(), (0, import_rxjs.tap)((item) => { this._pendingRequests.next(without(this._pendingRequests.getValue(), item)); }))); } consumeAllRequests() { const pendingRequests = this._pendingRequests.getValue(); this._pendingRequests.next([]); return pendingRequests; } getPendingRequests() { return this._pendingRequests.getValue().map((pendingRequest) => pendingRequest.requestInfo); } getPendingRequestCount(requestKind) { const pendingRequests = this._pendingRequests.getValue(); if (!requestKind) { return pendingRequests.length; } return pendingRequests.filter((pendingRequest) => pendingRequest.requestInfo.method === requestKind).length; } async authorize(requestKind) { const pendingRequest = await this.consumeRequest(requestKind); return pendingRequest.authorize(); } async reject(requestKind, reason = Deny()) { const pendingRequest = await this.consumeRequest(requestKind); return pendingRequest.reject(reason); } authorizeAll() { this.consumeAllRequests().forEach((request) => request.authorize()); } rejectAll(reason = Deny()) { this.consumeAllRequests().forEach((request) => request.reject(reason)); } async changeAccounts(signers) { this._signers = signers; this.emit("accountsChanged", await Promise.all(this._signers.map((signer) => signer.getAddress()))); } getCurrentChain() { const chainConn = this.chains.find((chainConn2) => chainConn2.chainId === this._activeChainId); if (!chainConn) { throw Disconnected(); } return chainConn; } getRpc() { const chainConn = this.getCurrentChain(); let rpc = this._rpc[chainConn.chainId]; if (!rpc) { rpc = new import_ethers.ethers.providers.JsonRpcProvider(chainConn.rpcUrl, chainConn.chainId); this._rpc[chainConn.chainId] = rpc; } return rpc; } getNetwork() { return this.getCurrentChain(); } getNetworks() { return this.chains; } addNetwork(chainId, rpcUrl) { this.chains.push({ chainId, rpcUrl }); } switchNetwork(chainId) { const idx = this.chains.findIndex((connection) => connection.chainId === chainId); if (idx < 0) { throw UnrecognizedChainID(); } if (chainId !== this._activeChainId) { this._activeChainId = chainId; this.emit("chainChanged", chainId); } } }; // src/ethers-kms-signer.ts var import_transactions = require("@ethersproject/transactions"); var import_ethers3 = require("ethers"); var import_utils = require("ethers/lib/utils"); var import_from_string = require("uint8arrays/from-string"); // src/functions.ts var import_ethers2 = require("ethers"); async function getAddressFromAgent(context, keyRef) { const publicKeyHex = await getKey(context, keyRef).then((key) => key?.publicKeyHex); if (!publicKeyHex) { throw Error(`Could not retrieve public hex key for ${keyRef}`); } const address = import_ethers2.ethers.utils.computeAddress(`${publicKeyHex.startsWith("0x") ? "" : "0x"}${publicKeyHex}`); if (!address || !address.startsWith("0x")) { throw Error(`Invalid address ${address} public key for key ${publicKeyHex}`); } return address; } __name(getAddressFromAgent, "getAddressFromAgent"); async function getKey(context, keyRef) { return await context.agent.keyManagerGet({ kid: keyRef.kid }); } __name(getKey, "getKey"); // src/ethers-kms-signer.ts var EthersKMSSignerBuilder = class { static { __name(this, "EthersKMSSignerBuilder"); } context; keyRef; provider; withContext(context) { this.context = context; return this; } withKid(kid) { this.keyRef = { kid }; return this; } withKeyRef(keyRef) { if (typeof keyRef === "string") { return this.withKid(keyRef); } this.keyRef = keyRef; return this; } withProvider(provider) { this.provider = provider; return this; } build() { if (!this.context) { throw Error("Agent context needs to be provided"); } if (!this.keyRef) { throw Error("Keyref needs to be provided"); } return new EthersKMSSigner({ context: this.context, keyRef: this.keyRef, provider: this.provider }); } }; var EthersKMSSigner = class _EthersKMSSigner extends import_ethers3.Signer { static { __name(this, "EthersKMSSigner"); } context; keyRef; constructor({ provider, context, keyRef }) { super(); (0, import_utils.defineReadOnly)(this, "provider", provider || void 0); this.context = context; this.keyRef = keyRef; } async getAddress() { return await getAddressFromAgent(this.context, this.keyRef); } async signTransaction(transaction) { const { from: from2, ...tx } = await transaction; return this.context.agent.keyManagerSign({ algorithm: "eth_signTransaction", keyRef: this.keyRef.kid, // keyRef: this.keyRef, // @ts-ignore data: (0, import_utils.arrayify)((0, import_transactions.serialize)(tx)) }); } async signRaw(message) { return await this.context.agent.keyManagerSign({ algorithm: "eth_rawSign", keyRef: this.keyRef.kid, encoding: "base16", // @ts-ignore // KMS accepts uint8arrays but interface does not expose it data: message }); } async signMessage(message) { return await this.context.agent.keyManagerSign({ algorithm: "eth_signMessage", keyRef: this.keyRef.kid, encoding: "base16", // @ts-ignore // KMS accepts uint8arrays but interface does not expose it data: message }); } async _signTypedData(domain, types, value) { const jsonData = { domain, types, message: value }; return this.context.agent.keyManagerSign({ algorithm: "eth_signTypedData", keyRef: this.keyRef.kid, // @ts-ignore // KMS accepts uint8arrays but interface does not expose it data: (0, import_from_string.fromString)(JSON.stringify(jsonData)) }); } connect(provider) { return new _EthersKMSSigner({ provider, context: this.context, keyRef: this.keyRef }); } }; // src/factory.ts function relayEvents(eventEmitter, execute) { const emit_ = eventEmitter.emit; eventEmitter.emit = (eventName, ...args) => { void execute("emit", eventName, ...args); return emit_.apply(eventEmitter, [ eventName, ...args ]); }; } __name(relayEvents, "relayEvents"); function createWeb3Provider(signers, chainId, rpcUrl, evaluate = async () => { }, config) { const chainIds = Array.isArray(chainId) ? chainId : [ chainId ]; const chains = chainIds.map((chainId2) => { return { chainId: chainId2, rpcUrl }; }); const web3Provider = new EthersHeadlessProvider(signers, chains, config); relayEvents(web3Provider, evaluate); return web3Provider; } __name(createWeb3Provider, "createWeb3Provider"); // src/rpc-server.ts var import_ssi_express_support = require("@sphereon/ssi-express-support"); var import_express = require("express"); function createRpcServer(provider, expressSupport, opts) { const express = expressSupport.express; const router = (0, import_express.Router)(); const path = opts?.path ?? "/web3/rpc"; console.log(`RPC server will use basePath ${opts?.basePath ?? "/"} and path ${path}`); router.post(path, (req, res, next) => { console.log(`REQ ${req.body?.method}:\r ${JSON.stringify(req.body, null, 2)}\r ===`); next(); }, async (req, res, next) => { try { const method = req.body.method; const params = req.body.params; const id = req.body.id; if (req.body.jsonrpc !== "2.0") { console.log("No valid JSON RPC call received", JSON.stringify(req.body)); return (0, import_ssi_express_support.sendErrorResponse)(res, 200, { id: req.body.id, jsonrpc: "2.0", error: "No valid JSON RPC call received. No jsonrp version supplied", code: -32600 }); } else if (!id || !method) { console.log("No valid JSON RPC call received", JSON.stringify(req.body)); return (0, import_ssi_express_support.sendErrorResponse)(res, 200, { id: req.body.id, jsonrpc: "2.0", error: "No valid JSON RPC call received", code: -32600 }); } const result = provider.request({ method, params }); provider.authorizeAll(); const respBody = { id, jsonrpc: "2.0", result: await result }; res.json(respBody); console.log(`RESPONSE for ${method}:\r ${JSON.stringify(respBody, null, 2)}`); } catch (error) { console.log(error.message); let msg = error.message; if (`body` in error) { msg = error.body; return (0, import_ssi_express_support.sendErrorResponse)(res, 200, msg); } else { return (0, import_ssi_express_support.sendErrorResponse)(res, 200, { id: req.body.id, jsonrpc: "2.0", error: msg, code: error.code ?? -32e3 }); } } return next(); }); express.use(opts?.basePath ?? "", router); } __name(createRpcServer, "createRpcServer"); function createServiceMethod(method, service, provider) { service[method] = async (params) => { const result = provider.request({ method, params }); provider.authorizeAll(); return await result; }; } __name(createServiceMethod, "createServiceMethod"); function createService(provider) { const service = {}; for (const method of Object.values(Web3Method)) { createServiceMethod(method, service, provider); } return service; } __name(createService, "createService"); //# sourceMappingURL=index.cjs.map