UNPKG

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

Version:

621 lines (611 loc) • 18.9 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // 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 import { toUtf8String } from "@ethersproject/strings"; import { signTypedData, SignTypedDataVersion } from "@metamask/eth-sig-util"; import assert from "assert/strict"; import { ethers } from "ethers"; import { BehaviorSubject, filter, first, firstValueFrom, from, switchMap, tap } from "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; _signers; _activeChainId; _rpc; _config; _authorizedRequests; constructor(signers, chains, config = {}) { super(), this.chains = chains, this._pendingRequests = new BehaviorSubject([]), this._signers = [], this._rpc = {}, this._authorizedRequests = {}; 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(); assert.equal(address, ethers.utils.getAddress(params[1])); const message = 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(); assert.equal(address, ethers.utils.getAddress(params[1])); const msgParams = params[0]; return signTypedData({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), data: msgParams, version: 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(); assert.equal(address, ethers.utils.getAddress(params[0])); const msgParams = JSON.parse(params[1]); return signTypedData({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), data: msgParams, version: method === "eth_signTypedData_v4" ? SignTypedDataVersion.V4 : 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 firstValueFrom(this._pendingRequests.pipe(switchMap((a) => from(a)), filter((request) => { return request.requestInfo.method === requestKind; }), first(), 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 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 import { serialize } from "@ethersproject/transactions"; import { Signer } from "ethers"; import { arrayify, defineReadOnly } from "ethers/lib/utils"; import { fromString } from "uint8arrays/from-string"; // src/functions.ts import { ethers as ethers2 } from "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 = ethers2.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 Signer { static { __name(this, "EthersKMSSigner"); } context; keyRef; constructor({ provider, context, keyRef }) { super(); 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: arrayify(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: 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 import { sendErrorResponse } from "@sphereon/ssi-express-support"; import { Router } from "express"; function createRpcServer(provider, expressSupport, opts) { const express = expressSupport.express; const router = 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 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 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 sendErrorResponse(res, 200, msg); } else { return 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"); export { EthersHeadlessProvider, EthersKMSSigner, EthersKMSSignerBuilder, EventEmitter, Web3Method, createRpcServer, createService, createServiceMethod, createWeb3Provider, getAddressFromAgent, getKey, without }; //# sourceMappingURL=index.js.map