@sphereon/ssi-sdk-web3.headless-provider
Version:
621 lines (611 loc) • 18.9 kB
JavaScript
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