Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

smoldot

Package Overview
Dependencies
Maintainers
1
Versions
104
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

smoldot - npm Package Compare versions

Comparing version 1.0.4 to 1.0.5

dist/cjs/bytecode-browser.d.ts

4

dist/cjs/index-browser.d.ts
/// <reference lib="dom" />
import { Client, ClientOptions } from './client.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './client.js';
import { Client, ClientOptions } from './public-types.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './public-types.js';
/**

@@ -5,0 +5,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

@@ -5,36 +5,13 @@ "use strict";

// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-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.start = exports.QueueFullError = exports.MalformedJsonRpcError = exports.JsonRpcDisabledError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0;
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// <reference lib="dom" />
const client_js_1 = require("./client.js");
const instance_js_1 = require("./instance/instance.js");
const base64_js_1 = require("./base64.js");
const pako_1 = require("pako");
const wasm_js_1 = require("./instance/autogen/wasm.js");
var client_js_2 = require("./client.js");
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return client_js_2.AddChainError; } });
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return client_js_2.AlreadyDestroyedError; } });
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return client_js_2.CrashError; } });
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return client_js_2.JsonRpcDisabledError; } });
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return client_js_2.MalformedJsonRpcError; } });
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return client_js_2.QueueFullError; } });
const no_auto_bytecode_browser_js_1 = require("./no-auto-bytecode-browser.js");
const bytecode_browser_js_1 = require("./bytecode-browser.js");
var public_types_js_1 = require("./public-types.js");
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } });
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } });
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return public_types_js_1.CrashError; } });
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return public_types_js_1.JsonRpcDisabledError; } });
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return public_types_js_1.MalformedJsonRpcError; } });
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return public_types_js_1.QueueFullError; } });
/**

@@ -49,478 +26,4 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

options = options || {};
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
const wasmModule = WebAssembly.compile((0, pako_1.inflate)((0, base64_js_1.classicDecode)(wasm_js_1.default)));
return (0, client_js_1.start)(options, wasmModule, {
registerShouldPeriodicallyYield: (callback) => {
if (typeof document === 'undefined') // We might be in a web worker.
return [false, () => { }];
const wrappedCallback = () => callback(document.visibilityState === 'visible');
document.addEventListener('visibilitychange', wrappedCallback);
return [document.visibilityState === 'visible', () => { document.removeEventListener('visibilitychange', wrappedCallback); }];
},
performanceNow: () => {
return performance.now();
},
getRandomValues: (buffer) => {
const crypto = globalThis.crypto;
if (!crypto)
throw new Error('randomness not available');
// Browsers have this completely undocumented behavior (it's not even part of a spec)
// that for some reason `getRandomValues` can't be called on arrayviews back by
// `SharedArrayBuffer`s and they throw an exception if you try.
if (buffer.buffer instanceof ArrayBuffer)
crypto.getRandomValues(buffer);
else {
const tmpArray = new Uint8Array(buffer.length);
crypto.getRandomValues(tmpArray);
buffer.set(tmpArray);
}
},
connect: (config) => {
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false, (options === null || options === void 0 ? void 0 : options.forbidWebRtc) || false);
}
});
return (0, no_auto_bytecode_browser_js_1.startWithBytecode)(Object.assign({ bytecode: (0, bytecode_browser_js_1.compileBytecode)() }, options));
}
exports.start = start;
/**
* Tries to open a new connection using the given configuration.
*
* @see Connection
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
*/
function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
// Attempt to parse the multiaddress.
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
const webRTCParsed = config.address.match(/^\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc-direct\/certhash\/(.*?)$/);
if (wsParsed != null) {
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
if ((proto == 'ws' && forbidWs) ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
(proto == 'wss' && forbidWss)) {
throw new instance_js_1.ConnectionError('Connection type not allowed');
}
const url = (wsParsed[1] == 'ip6') ?
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
const connection = new WebSocket(url);
connection.binaryType = 'arraybuffer';
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
const checkBufferedAmount = () => {
if (connection.readyState != 1)
return;
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
// of all the data that has been sent, but that might not be the case. For this
// reason, we use `bufferedAmount` as a hint rather than a correct value.
const bufferedAmount = connection.bufferedAmount;
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
if (wasSent < 0)
wasSent = 0;
bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
bufferedAmountCheck.nextTimeout *= 2;
if (bufferedAmountCheck.nextTimeout > 500)
bufferedAmountCheck.nextTimeout = 500;
}
// Note: it is important to call `onWritableBytes` at the very end, as it might
// trigger a call to `send`.
if (wasSent != 0)
config.onWritableBytes(wasSent);
};
connection.onopen = () => {
config.onOpen({
type: 'single-stream', handshake: 'multistream-select-noise-yamux',
initialWritableBytes: 1024 * 1024, writeClosable: false,
});
};
connection.onclose = (event) => {
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
config.onConnectionReset(message);
};
connection.onmessage = (msg) => {
config.onMessage(new Uint8Array(msg.data));
};
return {
reset: () => {
connection.onopen = null;
connection.onclose = null;
connection.onmessage = null;
connection.onerror = null;
connection.close();
},
send: (data) => {
connection.send(data);
if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
bufferedAmountCheck.nextTimeout = 10;
setTimeout(checkBufferedAmount, 10);
}
bufferedAmountCheck.quenedUnreportedBytes += data.length;
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else if (webRTCParsed != null) {
const targetPort = webRTCParsed[3];
if (forbidWebRTC || targetPort === '0') {
throw new instance_js_1.ConnectionError('Connection type not allowed');
}
const ipVersion = webRTCParsed[1] == 'ip4' ? '4' : '6';
const targetIp = webRTCParsed[2];
const remoteCertMultibase = webRTCParsed[4];
// The payload of `/certhash` is the hash of the self-generated certificate that the
// server presents.
// This function throws an exception if the certhash isn't correct. For this reason, this call
// is performed as part of the parsing of the multiaddr.
const remoteCertMultihash = (0, base64_js_1.multibaseBase64Decode)(remoteCertMultibase);
const remoteCertSha256Hash = multihashToSha256(remoteCertMultihash);
// TODO: detect localhost for Firefox? https://bugzilla.mozilla.org/show_bug.cgi?id=1659672
// Note that `pc` can be the connection, but also null or undefined.
// `undefined` means "certificate generation in progress", while `null` means "opening must
// be cancelled".
// While it would be better to use for example a string instead of `null`, using `null` lets
// us use the `!` operator more easily and leads to more readable code.
let pc = undefined;
// Contains the data channels that are open and have been reported to smoldot.
const dataChannels = new Map();
// For various reasons explained below, we open a data channel in advance without reporting it
// to smoldot. This data channel is stored in this variable. Once it is reported to smoldot,
// it is inserted in `dataChannels`.
let handshakeDataChannel;
// Multihash-encoded DTLS certificate of the local node. Unknown as long as it hasn't been
// generated.
// TODO: could be merged with `pc` in one variable, and maybe even the other fields as well
let localTlsCertificateMultihash;
// Kills all the JavaScript objects (the connection and all its substreams), ensuring that no
// callback will be called again. Doesn't report anything to smoldot, as this should be done
// by the caller.
const killAllJs = () => {
// The `RTCPeerConnection` is created pretty quickly. It is however still possible for
// smoldot to cancel the opening, in which case `pc` will still be undefined.
if (!pc) {
console.assert(dataChannels.size === 0 && !handshakeDataChannel, "substreams exist while pc is undef");
pc = null;
return;
}
pc.onconnectionstatechange = null;
pc.onnegotiationneeded = null;
pc.ondatachannel = null;
for (const channel of Array.from(dataChannels.values())) {
channel.channel.onopen = null;
channel.channel.onerror = null;
channel.channel.onclose = null;
channel.channel.onbufferedamountlow = null;
channel.channel.onmessage = null;
}
dataChannels.clear();
if (handshakeDataChannel) {
handshakeDataChannel.onopen = null;
handshakeDataChannel.onerror = null;
handshakeDataChannel.onclose = null;
handshakeDataChannel.onbufferedamountlow = null;
handshakeDataChannel.onmessage = null;
}
handshakeDataChannel = undefined;
pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
};
// Function that configures a newly-opened channel and adds it to the map. Used for both
// inbound and outbound substreams.
const addChannel = (dataChannel, direction) => {
const dataChannelId = dataChannel.id;
dataChannel.binaryType = 'arraybuffer';
let isOpen = false;
dataChannel.onopen = () => {
console.assert(!isOpen, "substream opened twice");
isOpen = true;
if (direction === 'first-outbound') {
console.assert(dataChannels.size === 0, "dataChannels not empty when opening");
console.assert(handshakeDataChannel === dataChannel, "handshake substream mismatch");
config.onOpen({
type: 'multi-stream',
handshake: 'webrtc',
// `addChannel` can never be called before the local certificate is generated, so this
// value is always defined.
localTlsCertificateMultihash: localTlsCertificateMultihash,
remoteTlsCertificateMultihash: remoteCertMultihash
});
}
else {
console.assert(direction !== 'outbound' || !handshakeDataChannel, "handshakeDataChannel still defined");
config.onStreamOpened(dataChannelId, direction, 65536);
}
};
dataChannel.onerror = dataChannel.onclose = (_error) => {
// A couple of different things could be happening here.
if (handshakeDataChannel === dataChannel && !isOpen) {
// The handshake data channel that we have opened ahead of time failed to open. As this
// happens before we have reported the WebRTC connection as a whole as being open, we
// need to report that the connection has failed to open.
killAllJs();
// Note that the event doesn't give any additional reason for the failure.
config.onConnectionReset("handshake data channel failed to open");
}
else if (handshakeDataChannel === dataChannel) {
// The handshake data channel has been closed before we reported it to smoldot. This
// isn't really a problem. We just update the state and continue running. If smoldot
// requests a substream, another one will be opened. It could be a valid implementation
// to also just kill the entire connection, however doing so is a bit too intrusive and
// punches through abstraction layers.
handshakeDataChannel.onopen = null;
handshakeDataChannel.onerror = null;
handshakeDataChannel.onclose = null;
handshakeDataChannel.onbufferedamountlow = null;
handshakeDataChannel.onmessage = null;
handshakeDataChannel = undefined;
}
else if (!isOpen) {
// Substream wasn't opened yet and thus has failed to open. The API has no mechanism to
// report substream openings failures. We could try opening it again, but given that
// it's unlikely to succeed, we simply opt to kill the entire connection.
killAllJs();
// Note that the event doesn't give any additional reason for the failure.
config.onConnectionReset("data channel failed to open");
}
else {
// Substream was open and is now closed. Normal situation.
config.onStreamReset(dataChannelId);
}
};
dataChannel.onbufferedamountlow = () => {
const channel = dataChannels.get(dataChannelId);
const val = channel.bufferedBytes;
channel.bufferedBytes = 0;
config.onWritableBytes(val, dataChannelId);
};
dataChannel.onmessage = (m) => {
// The `data` field is an `ArrayBuffer`.
config.onMessage(new Uint8Array(m.data), dataChannelId);
};
if (direction !== 'first-outbound')
dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 });
else
handshakeDataChannel = dataChannel;
};
// It is possible for the browser to use multiple different certificates.
// In order for our local certificate to be deterministic, we need to generate it manually and
// set it explicitly as part of the configuration.
// According to <https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate>,
// browsers are guaranteed to support `{ name: "ECDSA", namedCurve: "P-256" }`.
RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () {
if (pc === null)
return;
// Create a new WebRTC connection.
pc = new RTCPeerConnection({ certificates: [localCertificate] });
// We need to build the multihash corresponding to the local certificate.
// While there exists a `RTCPeerConnection.getFingerprints` function, Firefox notably
// doesn't support it.
// See <https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate#browser_compatibility>
// An alternative to `getFingerprints` is to ask the browser to generate an SDP offer and
// extract from fingerprint from it. Because we explicitly provide a certificate, we have
// the guarantee that the list of certificates will always be the same whenever an SDP offer
// is generated by the browser. However, while this alternative does work on Firefox, it
// doesn't on Chrome, as the SDP offer is for some reason missing the fingerprints.
// Therefore, our strategy is to use `getFingerprints` when it is available (i.e. every
// browser except Firefox), and parse the SDP offer when it is not (i.e. Firefox). In the
// future, only `getFingerprints` would be used.
let localTlsCertificateHex;
if (localCertificate.getFingerprints) {
for (const { algorithm, value } of localCertificate.getFingerprints()) {
if (algorithm === 'sha-256') {
localTlsCertificateHex = value;
break;
}
}
}
else {
const localSdpOffer = yield pc.createOffer();
// Note that this regex is not strict. The browser isn't a malicious actor, and the
// objective of this regex is not to detect invalid input.
const localSdpOfferFingerprintMatch = localSdpOffer.sdp.match(/a(\s*)=(\s*)fingerprint:(\s*)(sha|SHA)-256(\s*)(([a-fA-F0-9]{2}(:)*){32})/);
if (localSdpOfferFingerprintMatch) {
localTlsCertificateHex = localSdpOfferFingerprintMatch[6];
}
}
if (localTlsCertificateHex === undefined) {
// Because we've already returned from the `connect` function at this point, we pretend
// that the connection has failed to open.
config.onConnectionReset('Failed to obtain the browser certificate fingerprint');
return;
}
localTlsCertificateMultihash = new Uint8Array(34);
localTlsCertificateMultihash.set([0x12, 32], 0);
localTlsCertificateMultihash.set(localTlsCertificateHex.split(':').map((s) => parseInt(s, 16)), 2);
// `onconnectionstatechange` is used to detect when the connection has closed or has failed
// to open.
// Note that smoldot will think that the connection is open even when it is still opening.
// Therefore we don't care about events concerning the fact that the connection is now fully
// open.
pc.onconnectionstatechange = (_event) => {
if (pc.connectionState == "closed" || pc.connectionState == "disconnected" || pc.connectionState == "failed") {
killAllJs();
config.onConnectionReset("WebRTC state transitioned to " + pc.connectionState);
}
};
pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () {
var _a;
// Create a new offer and set it as local description.
let sdpOffer = (yield pc.createOffer()).sdp;
// We check that the locally-generated SDP offer has a data channel with the UDP
// protocol. If that isn't the case, the connection will likely fail.
if (sdpOffer.match(/^m=application(\s+)(\d+)(\s+)UDP\/DTLS\/SCTP(\s+)webrtc-datachannel$/m) === null) {
console.error("Local offer doesn't contain UDP data channel. WebRTC connections will likely fail. Please report this issue.");
}
// According to the libp2p WebRTC spec, the ufrag and pwd are the same
// randomly-generated string on both sides, and must be prefixed with
// `libp2p-webrtc-v1:`. We modify the local description to ensure that.
// While we could randomly generate a new string, we just grab the one that the
// browser has generated, in order to make sure that it respects the constraints
// of the ICE protocol.
const browserGeneratedPwd = (_a = sdpOffer.match(/^a=ice-pwd:(.+)$/m)) === null || _a === void 0 ? void 0 : _a.at(1);
if (browserGeneratedPwd === undefined) {
console.error("Failed to set ufrag to pwd. WebRTC connections will likely fail. Please report this issue.");
}
const ufragPwd = "libp2p+webrtc+v1/" + browserGeneratedPwd;
sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd);
sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd);
yield pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
// Transform certificate hash into fingerprint (upper-hex; each byte separated by ":").
const fingerprint = Array.from(remoteCertSha256Hash).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':');
// Note that the trailing line feed is important, as otherwise Chrome
// fails to parse the payload.
const remoteSdp =
// Version of the SDP protocol. Always 0. (RFC8866)
"v=0" + "\n" +
// Identifies the creator of the SDP document. We are allowed to use dummy values
// (`-` and `0.0.0.0`) to remain anonymous, which we do. Note that "IN" means
// "Internet" (and not "input"). (RFC8866)
"o=- 0 0 IN IP" + ipVersion + " " + targetIp + "\n" +
// Name for the session. We are allowed to pass a dummy `-`. (RFC8866)
"s=-" + "\n" +
// Start and end of the validity of the session. `0 0` means that the session never
// expires. (RFC8866)
"t=0 0" + "\n" +
// A lite implementation is only appropriate for devices that will
// always be connected to the public Internet and have a public
// IP address at which it can receive packets from any
// correspondent. ICE will not function when a lite implementation
// is placed behind a NAT (RFC8445).
"a=ice-lite" + "\n" +
// A `m=` line describes a request to establish a certain protocol.
// The protocol in this line (i.e. `TCP/DTLS/SCTP` or `UDP/DTLS/SCTP`) must always be
// the same as the one in the offer. We know that this is true because checked above.
// The `<fmt>` component must always be `webrtc-datachannel` for WebRTC.
// The rest of the SDP payload adds attributes to this specific media stream.
// RFCs: 8839, 8866, 8841
"m=application " + targetPort + " " + "UDP/DTLS/SCTP webrtc-datachannel" + "\n" +
// Indicates the IP address of the remote.
// Note that "IN" means "Internet" (and not "input").
"c=IN IP" + ipVersion + " " + targetIp + "\n" +
// Media ID - uniquely identifies this media stream (RFC9143).
"a=mid:0" + "\n" +
// Indicates that we are complying with RFC8839 (as oppposed to the legacy RFC5245).
"a=ice-options:ice2" + "\n" +
// ICE username and password, which are used for establishing and
// maintaining the ICE connection. (RFC8839)
// These values are set according to the libp2p WebRTC specification.
"a=ice-ufrag:" + ufragPwd + "\n" +
"a=ice-pwd:" + ufragPwd + "\n" +
// Fingerprint of the certificate that the server will use during the TLS
// handshake. (RFC8122)
// MUST be derived from the certificate used by the answerer (server).
"a=fingerprint:sha-256 " + fingerprint + "\n" +
// Indicates that the remote DTLS server will only listen for incoming
// connections. (RFC5763)
// The answerer (server) MUST not be located behind a NAT (RFC6135).
"a=setup:passive" + "\n" +
// The SCTP port (RFC8841)
// Note it's different from the "m=" line port value, which
// indicates the port of the underlying transport-layer protocol
// (UDP or TCP)
"a=sctp-port:5000" + "\n" +
// The maximum SCTP user message size (in bytes) (RFC8841)
// Setting this field is part of the libp2p spec.
"a=max-message-size:16384" + "\n" +
// A transport address for a candidate that can be used for connectivity
// checks (RFC8839).
"a=candidate:1 1 UDP 1 " + targetIp + " " + targetPort + " typ host" + "\n";
yield pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });
});
pc.ondatachannel = ({ channel }) => {
// TODO: is the substream maybe already open? according to the Internet it seems that no but it's unclear
addChannel(channel, 'inbound');
};
// Creating a `RTCPeerConnection` doesn't actually do anything before `createDataChannel` is
// called. Smoldot's API, however, requires you to treat entire connections as open or
// closed. We know, according to the libp2p WebRTC specification, that every connection
// always starts with a substream where a handshake is performed. After we've reported that
// the connection is open, smoldot will open a substream in order to perform the handshake.
// Instead of following this API, we open this substream in advance, and will notify smoldot
// that the connection is open when the substream is open.
// Note that the label passed to `createDataChannel` is required to be empty as per the
// libp2p WebRTC specification.
addChannel(pc.createDataChannel("", { id: 0, negotiated: true }), 'first-outbound');
}));
return {
reset: (streamId) => {
// If `streamId` is undefined, then the whole connection must be destroyed.
if (streamId === undefined) {
killAllJs();
}
else {
const channel = dataChannels.get(streamId);
channel.channel.onopen = null;
channel.channel.onerror = null;
channel.channel.onclose = null;
channel.channel.onbufferedamountlow = null;
channel.channel.onmessage = null;
channel.channel.close();
dataChannels.delete(streamId);
}
},
send: (data, streamId) => {
const channel = dataChannels.get(streamId);
channel.channel.send(data);
channel.bufferedBytes += data.length;
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => {
// `openOutSubstream` can only be called after we have called `config.onOpen`, therefore
// `pc` is guaranteed to be non-null.
// As explained above, we open a data channel ahead of time. If this data channel is still
// there, we report it.
if (handshakeDataChannel) {
// Do this asynchronously because calling callbacks within callbacks is error-prone.
(() => __awaiter(this, void 0, void 0, function* () {
// We need to check again if `handshakeDataChannel` is still defined, as the
// connection might have been closed.
if (handshakeDataChannel) {
config.onStreamOpened(handshakeDataChannel.id, 'outbound', 1024 * 1024);
dataChannels.set(handshakeDataChannel.id, { channel: handshakeDataChannel, bufferedBytes: 0 });
handshakeDataChannel = undefined;
}
}))();
}
else {
// Note that the label passed to `createDataChannel` is required to be empty as per the
// libp2p WebRTC specification.
addChannel(pc.createDataChannel(""), 'outbound');
}
}
};
}
else {
throw new instance_js_1.ConnectionError('Unrecognized multiaddr format');
}
}
/// Parses a multihash-multibase-encoded string into a SHA256 hash.
///
/// Throws an exception if the multihash algorithm isn't SHA256.
const multihashToSha256 = (certMultihash) => {
if (certMultihash.length != 34 || certMultihash[0] != 0x12 || certMultihash[1] != 32) {
throw new Error('Certificate multihash is not SHA-256');
}
return new Uint8Array(certMultihash.slice(2));
};

@@ -1,3 +0,3 @@

import { Client, ClientOptions } from './client.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js';
import { Client, ClientOptions } from './public-types.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js';
/**

@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

@@ -5,33 +5,13 @@ "use strict";

// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-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.start = exports.JsonRpcDisabledError = exports.QueueFullError = exports.MalformedJsonRpcError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0;
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const client_js_1 = require("./client.js");
const instance_js_1 = require("./instance/instance.js");
const wasm_js_1 = require("./instance/autogen/wasm.js");
var client_js_2 = require("./client.js");
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return client_js_2.AddChainError; } });
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return client_js_2.AlreadyDestroyedError; } });
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return client_js_2.CrashError; } });
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return client_js_2.MalformedJsonRpcError; } });
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return client_js_2.QueueFullError; } });
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return client_js_2.JsonRpcDisabledError; } });
const no_auto_bytecode_deno_js_1 = require("./no-auto-bytecode-deno.js");
const bytecode_deno_js_1 = require("./bytecode-deno.js");
var public_types_js_1 = require("./public-types.js");
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } });
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } });
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return public_types_js_1.CrashError; } });
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return public_types_js_1.MalformedJsonRpcError; } });
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return public_types_js_1.QueueFullError; } });
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return public_types_js_1.JsonRpcDisabledError; } });
/**

@@ -46,257 +26,4 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

options = options || {};
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
const wasmModule = zlibInflate(trustedBase64Decode(wasm_js_1.default)).then(((bytecode) => WebAssembly.compile(bytecode)));
return (0, client_js_1.start)(options || {}, wasmModule, {
registerShouldPeriodicallyYield: (_callback) => {
return [true, () => { }];
},
performanceNow: () => {
return performance.now();
},
getRandomValues: (buffer) => {
const crypto = globalThis.crypto;
if (!crypto)
throw new Error('randomness not available');
crypto.getRandomValues(buffer);
},
connect: (config) => {
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false);
}
});
return (0, no_auto_bytecode_deno_js_1.startWithBytecode)(Object.assign({ bytecode: (0, bytecode_deno_js_1.compileBytecode)() }, options));
}
exports.start = start;
/**
* Applies the zlib inflate algorithm on the buffer.
*/
function zlibInflate(buffer) {
return __awaiter(this, void 0, void 0, function* () {
// This code has been copy-pasted from the official streams draft specification.
// At the moment, it is found here: https://wicg.github.io/compression/#example-deflate-compress
const ds = new DecompressionStream('deflate');
const writer = ds.writable.getWriter();
writer.write(buffer);
writer.close();
const output = [];
const reader = ds.readable.getReader();
let totalSize = 0;
while (true) {
const { value, done } = yield reader.read();
if (done)
break;
output.push(value);
totalSize += value.byteLength;
}
const concatenated = new Uint8Array(totalSize);
let offset = 0;
for (const array of output) {
concatenated.set(array, offset);
offset += array.byteLength;
}
return concatenated;
});
}
/**
* Decodes a base64 string.
*
* The input is assumed to be correct.
*/
function trustedBase64Decode(base64) {
// This code is a bit sketchy due to the fact that we decode into a string, but it seems to
// work.
const binaryString = atob(base64);
const size = binaryString.length;
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
/**
* Tries to open a new connection using the given configuration.
*
* @see Connection
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
*/
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
// Attempt to parse the multiaddress.
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/);
if (wsParsed != null) {
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
if ((proto == 'ws' && forbidWs) ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
(proto == 'wss' && forbidWss)) {
throw new instance_js_1.ConnectionError('Connection type not allowed');
}
const url = (wsParsed[1] == 'ip6') ?
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
const socket = new WebSocket(url);
socket.binaryType = 'arraybuffer';
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
const checkBufferedAmount = () => {
if (socket.readyState != 1)
return;
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
// of all the data that has been sent, but that might not be the case. For this
// reason, we use `bufferedAmount` as a hint rather than a correct value.
const bufferedAmount = socket.bufferedAmount;
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
if (wasSent < 0)
wasSent = 0;
bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
bufferedAmountCheck.nextTimeout *= 2;
if (bufferedAmountCheck.nextTimeout > 500)
bufferedAmountCheck.nextTimeout = 500;
}
// Note: it is important to call `onWritableBytes` at the very end, as it might
// trigger a call to `send`.
if (wasSent != 0)
config.onWritableBytes(wasSent);
};
socket.onopen = () => {
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false });
};
socket.onclose = (event) => {
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
config.onConnectionReset(message);
};
socket.onmessage = (msg) => {
config.onMessage(new Uint8Array(msg.data));
};
return {
reset: () => {
// We can't set these fields to null because the TypeScript definitions don't
// allow it, but we can set them to dummy values.
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
socket.close();
},
send: (data) => {
// The WebSocket library that we use seems to spontaneously transition connections
// to the "closed" state but not call the `onclosed` callback immediately. Calling
// `send` on that object throws an exception. In order to avoid panicking smoldot,
// we thus absorb any exception thrown here.
// See also <https://github.com/paritytech/smoldot/issues/2937>.
try {
socket.send(data);
if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
bufferedAmountCheck.nextTimeout = 10;
setTimeout(checkBufferedAmount, 10);
}
bufferedAmountCheck.quenedUnreportedBytes += data.length;
}
catch (_error) { }
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else if (tcpParsed != null) {
if (forbidTcp) {
throw new instance_js_1.ConnectionError('TCP connections not available');
}
const socket = {
destroyed: false,
inner: Deno.connect({
hostname: tcpParsed[2],
port: parseInt(tcpParsed[3], 10),
}).catch((error) => {
socket.destroyed = true;
config.onConnectionReset(error.toString());
return null;
})
};
socket.inner = socket.inner.then((established) => {
// TODO: at the time of writing of this comment, `setNoDelay` is still unstable
//established.setNoDelay();
if (socket.destroyed)
return established;
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: true });
// Spawns an asynchronous task that continuously reads from the socket.
// Every time data is read, the task re-executes itself in order to continue reading.
// The task ends automatically if an EOF or error is detected, which should also happen
// if the user calls `close()`.
const read = (readBuffer) => __awaiter(this, void 0, void 0, function* () {
if (socket.destroyed || established === null)
return;
let outcome = null;
try {
outcome = yield established.read(readBuffer);
}
catch (error) {
// The type of `error` is unclear, but we assume that it implements `Error`
outcome = error.toString();
}
if (socket.destroyed)
return;
if (typeof outcome !== 'number' || outcome === null) {
// The socket is reported closed, but `socket.destroyed` is still `false` (see
// check above). As such, we must inform the inner layers.
socket.destroyed = true;
config.onConnectionReset(outcome === null ? "EOF when reading socket" : outcome);
return;
}
console.assert(outcome !== 0); // `read` guarantees to return a non-zero value.
config.onMessage(readBuffer.slice(0, outcome));
return read(readBuffer);
});
read(new Uint8Array(32768));
return established;
});
return {
reset: () => {
socket.destroyed = true;
socket.inner.then((connec) => connec.close());
},
send: (data) => {
let dataCopy = Uint8Array.from(data); // Deep copy of the data
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
while (dataCopy.length > 0) {
if (socket.destroyed || c === null)
return c;
let outcome;
try {
outcome = yield c.write(dataCopy);
config.onWritableBytes(dataCopy.length);
}
catch (error) {
// The type of `error` is unclear, but we assume that it implements `Error`
outcome = error.toString();
}
if (typeof outcome !== 'number') {
// The socket is reported closed, but `socket.destroyed` is still
// `false` (see check above). As such, we must inform the inner layers.
socket.destroyed = true;
config.onConnectionReset(outcome);
return c;
}
// Note that, contrary to `read`, it is possible for `outcome` to be 0.
// This happen if the write had to be interrupted, and the only thing
// we have to do is try writing again.
dataCopy = dataCopy.slice(outcome);
}
return c;
}));
},
closeSend: () => {
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
yield (c === null || c === void 0 ? void 0 : c.closeWrite());
return c;
}));
},
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else {
throw new instance_js_1.ConnectionError('Unrecognized multiaddr format');
}
}

@@ -1,3 +0,3 @@

import { Client, ClientOptions } from './client.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js';
import { Client, ClientOptions } from './public-types.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js';
/**

@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

@@ -7,30 +7,11 @@ "use strict";

exports.start = exports.JsonRpcDisabledError = exports.QueueFullError = exports.MalformedJsonRpcError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0;
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Note: if you modify these imports, please test both the ModuleJS and CommonJS generated
// bindings. JavaScript being JavaScript, some libraries (such as `websocket`) have issues working
// with both at the same time.
const client_js_1 = require("./client.js");
const instance_js_1 = require("./instance/instance.js");
const wasm_js_1 = require("./instance/autogen/wasm.js");
const ws_1 = require("ws");
const pako_1 = require("pako");
const node_perf_hooks_1 = require("node:perf_hooks");
const node_net_1 = require("node:net");
const node_crypto_1 = require("node:crypto");
var client_js_2 = require("./client.js");
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return client_js_2.AddChainError; } });
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return client_js_2.AlreadyDestroyedError; } });
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return client_js_2.CrashError; } });
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return client_js_2.MalformedJsonRpcError; } });
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return client_js_2.QueueFullError; } });
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return client_js_2.JsonRpcDisabledError; } });
const no_auto_bytecode_nodejs_js_1 = require("./no-auto-bytecode-nodejs.js");
const bytecode_nodejs_js_1 = require("./bytecode-nodejs.js");
var public_types_js_1 = require("./public-types.js");
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } });
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } });
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return public_types_js_1.CrashError; } });
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return public_types_js_1.MalformedJsonRpcError; } });
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return public_types_js_1.QueueFullError; } });
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return public_types_js_1.JsonRpcDisabledError; } });
/**

@@ -45,186 +26,4 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

options = options || {};
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
const wasmModule = WebAssembly.compile((0, pako_1.inflate)(Buffer.from(wasm_js_1.default, 'base64')));
return (0, client_js_1.start)(options || {}, wasmModule, {
registerShouldPeriodicallyYield: (_callback) => {
return [true, () => { }];
},
performanceNow: () => {
return node_perf_hooks_1.performance.now();
},
getRandomValues: (buffer) => {
if (buffer.length >= 1024 * 1024)
throw new Error('getRandomValues buffer too large');
(0, node_crypto_1.randomFillSync)(buffer);
},
connect: (config) => {
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false);
}
});
return (0, no_auto_bytecode_nodejs_js_1.startWithBytecode)(Object.assign({ bytecode: (0, bytecode_nodejs_js_1.compileBytecode)() }, options));
}
exports.start = start;
/**
* Tries to open a new connection using the given configuration.
*
* @see Connection
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
*/
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
// Attempt to parse the multiaddress.
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/);
if (wsParsed != null) {
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
if ((proto == 'ws' && forbidWs) ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
(proto == 'wss' && forbidWss)) {
throw new instance_js_1.ConnectionError('Connection type not allowed');
}
const url = (wsParsed[1] == 'ip6') ?
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
const socket = new ws_1.WebSocket(url);
socket.binaryType = 'arraybuffer';
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
const checkBufferedAmount = () => {
if (socket.readyState != 1)
return;
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
// of all the data that has been sent, but that seems to not be the case. It is
// unclear whether this is intended or a bug, but is is likely that `bufferedAmount`
// also includes WebSocket headers. For this reason, we use `bufferedAmount` as a hint
// rather than a correct value.
const bufferedAmount = socket.bufferedAmount;
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
if (wasSent < 0)
wasSent = 0;
bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
bufferedAmountCheck.nextTimeout *= 2;
if (bufferedAmountCheck.nextTimeout > 500)
bufferedAmountCheck.nextTimeout = 500;
}
// Note: it is important to call `onWritableBytes` at the very end, as it might
// trigger a call to `send`.
if (wasSent != 0)
config.onWritableBytes(wasSent);
};
socket.onopen = () => {
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false });
};
socket.onclose = (event) => {
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
config.onConnectionReset(message);
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
};
socket.onerror = (event) => {
config.onConnectionReset(event.message);
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
};
socket.onmessage = (msg) => {
config.onMessage(new Uint8Array(msg.data));
};
return {
reset: () => {
// We can't set these fields to null because the TypeScript definitions don't
// allow it, but we can set them to dummy values.
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
socket.close();
},
send: (data) => {
socket.send(data);
if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
bufferedAmountCheck.nextTimeout = 10;
setTimeout(checkBufferedAmount, 10);
}
bufferedAmountCheck.quenedUnreportedBytes += data.length;
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else if (tcpParsed != null) {
// `net` module will be missing when we're not in NodeJS.
if (forbidTcp) {
throw new instance_js_1.ConnectionError('TCP connections not available');
}
const socket = (0, node_net_1.createConnection)({
host: tcpParsed[2],
port: parseInt(tcpParsed[3], 10),
});
// Number of bytes queued using `socket.write` and where `write` has returned false.
const drainingBytes = { num: 0 };
socket.setNoDelay();
socket.on('connect', () => {
if (socket.destroyed)
return;
config.onOpen({
type: 'single-stream', handshake: 'multistream-select-noise-yamux',
initialWritableBytes: socket.writableHighWaterMark, writeClosable: true
});
});
socket.on('close', (hasError) => {
if (socket.destroyed)
return;
// NodeJS doesn't provide a reason why the closing happened, but only
// whether it was caused by an error.
const message = hasError ? "Error" : "Closed gracefully";
config.onConnectionReset(message);
});
socket.on('error', () => { });
socket.on('data', (message) => {
if (socket.destroyed)
return;
config.onMessage(new Uint8Array(message.buffer));
});
socket.on('drain', () => {
// The bytes queued using `socket.write` and where `write` has returned false have now
// been sent. Notify the API that it can write more data.
if (socket.destroyed)
return;
const val = drainingBytes.num;
drainingBytes.num = 0;
config.onWritableBytes(val);
});
return {
reset: () => {
socket.destroy();
},
send: (data) => {
const dataLen = data.length;
const allWritten = socket.write(data);
if (allWritten) {
setImmediate(() => {
if (!socket.writable)
return;
config.onWritableBytes(dataLen);
});
}
else {
drainingBytes.num += dataLen;
}
},
closeSend: () => {
socket.end();
},
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else {
throw new instance_js_1.ConnectionError('Unrecognized multiaddr format');
}
}
/// <reference lib="dom" />
import { Client, ClientOptions } from './client.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './client.js';
import { Client, ClientOptions } from './public-types.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './public-types.js';
/**

@@ -5,0 +5,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

// Smoldot
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-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());
});
};
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// <reference lib="dom" />
import { start as innerStart } from './client.js';
import { ConnectionError } from './instance/instance.js';
import { classicDecode, multibaseBase64Decode } from './base64.js';
import { inflate } from 'pako';
import { default as wasmBase64 } from './instance/autogen/wasm.js';
export { AddChainError, AlreadyDestroyedError, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError } from './client.js';
import { startWithBytecode } from './no-auto-bytecode-browser.js';
import { compileBytecode } from './bytecode-browser.js';
export { AddChainError, AlreadyDestroyedError, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError } from './public-types.js';
/**

@@ -39,477 +16,3 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

options = options || {};
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
const wasmModule = WebAssembly.compile(inflate(classicDecode(wasmBase64)));
return innerStart(options, wasmModule, {
registerShouldPeriodicallyYield: (callback) => {
if (typeof document === 'undefined') // We might be in a web worker.
return [false, () => { }];
const wrappedCallback = () => callback(document.visibilityState === 'visible');
document.addEventListener('visibilitychange', wrappedCallback);
return [document.visibilityState === 'visible', () => { document.removeEventListener('visibilitychange', wrappedCallback); }];
},
performanceNow: () => {
return performance.now();
},
getRandomValues: (buffer) => {
const crypto = globalThis.crypto;
if (!crypto)
throw new Error('randomness not available');
// Browsers have this completely undocumented behavior (it's not even part of a spec)
// that for some reason `getRandomValues` can't be called on arrayviews back by
// `SharedArrayBuffer`s and they throw an exception if you try.
if (buffer.buffer instanceof ArrayBuffer)
crypto.getRandomValues(buffer);
else {
const tmpArray = new Uint8Array(buffer.length);
crypto.getRandomValues(tmpArray);
buffer.set(tmpArray);
}
},
connect: (config) => {
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false, (options === null || options === void 0 ? void 0 : options.forbidWebRtc) || false);
}
});
return startWithBytecode(Object.assign({ bytecode: compileBytecode() }, options));
}
/**
* Tries to open a new connection using the given configuration.
*
* @see Connection
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
*/
function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
// Attempt to parse the multiaddress.
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
const webRTCParsed = config.address.match(/^\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc-direct\/certhash\/(.*?)$/);
if (wsParsed != null) {
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
if ((proto == 'ws' && forbidWs) ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
(proto == 'wss' && forbidWss)) {
throw new ConnectionError('Connection type not allowed');
}
const url = (wsParsed[1] == 'ip6') ?
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
const connection = new WebSocket(url);
connection.binaryType = 'arraybuffer';
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
const checkBufferedAmount = () => {
if (connection.readyState != 1)
return;
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
// of all the data that has been sent, but that might not be the case. For this
// reason, we use `bufferedAmount` as a hint rather than a correct value.
const bufferedAmount = connection.bufferedAmount;
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
if (wasSent < 0)
wasSent = 0;
bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
bufferedAmountCheck.nextTimeout *= 2;
if (bufferedAmountCheck.nextTimeout > 500)
bufferedAmountCheck.nextTimeout = 500;
}
// Note: it is important to call `onWritableBytes` at the very end, as it might
// trigger a call to `send`.
if (wasSent != 0)
config.onWritableBytes(wasSent);
};
connection.onopen = () => {
config.onOpen({
type: 'single-stream', handshake: 'multistream-select-noise-yamux',
initialWritableBytes: 1024 * 1024, writeClosable: false,
});
};
connection.onclose = (event) => {
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
config.onConnectionReset(message);
};
connection.onmessage = (msg) => {
config.onMessage(new Uint8Array(msg.data));
};
return {
reset: () => {
connection.onopen = null;
connection.onclose = null;
connection.onmessage = null;
connection.onerror = null;
connection.close();
},
send: (data) => {
connection.send(data);
if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
bufferedAmountCheck.nextTimeout = 10;
setTimeout(checkBufferedAmount, 10);
}
bufferedAmountCheck.quenedUnreportedBytes += data.length;
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else if (webRTCParsed != null) {
const targetPort = webRTCParsed[3];
if (forbidWebRTC || targetPort === '0') {
throw new ConnectionError('Connection type not allowed');
}
const ipVersion = webRTCParsed[1] == 'ip4' ? '4' : '6';
const targetIp = webRTCParsed[2];
const remoteCertMultibase = webRTCParsed[4];
// The payload of `/certhash` is the hash of the self-generated certificate that the
// server presents.
// This function throws an exception if the certhash isn't correct. For this reason, this call
// is performed as part of the parsing of the multiaddr.
const remoteCertMultihash = multibaseBase64Decode(remoteCertMultibase);
const remoteCertSha256Hash = multihashToSha256(remoteCertMultihash);
// TODO: detect localhost for Firefox? https://bugzilla.mozilla.org/show_bug.cgi?id=1659672
// Note that `pc` can be the connection, but also null or undefined.
// `undefined` means "certificate generation in progress", while `null` means "opening must
// be cancelled".
// While it would be better to use for example a string instead of `null`, using `null` lets
// us use the `!` operator more easily and leads to more readable code.
let pc = undefined;
// Contains the data channels that are open and have been reported to smoldot.
const dataChannels = new Map();
// For various reasons explained below, we open a data channel in advance without reporting it
// to smoldot. This data channel is stored in this variable. Once it is reported to smoldot,
// it is inserted in `dataChannels`.
let handshakeDataChannel;
// Multihash-encoded DTLS certificate of the local node. Unknown as long as it hasn't been
// generated.
// TODO: could be merged with `pc` in one variable, and maybe even the other fields as well
let localTlsCertificateMultihash;
// Kills all the JavaScript objects (the connection and all its substreams), ensuring that no
// callback will be called again. Doesn't report anything to smoldot, as this should be done
// by the caller.
const killAllJs = () => {
// The `RTCPeerConnection` is created pretty quickly. It is however still possible for
// smoldot to cancel the opening, in which case `pc` will still be undefined.
if (!pc) {
console.assert(dataChannels.size === 0 && !handshakeDataChannel, "substreams exist while pc is undef");
pc = null;
return;
}
pc.onconnectionstatechange = null;
pc.onnegotiationneeded = null;
pc.ondatachannel = null;
for (const channel of Array.from(dataChannels.values())) {
channel.channel.onopen = null;
channel.channel.onerror = null;
channel.channel.onclose = null;
channel.channel.onbufferedamountlow = null;
channel.channel.onmessage = null;
}
dataChannels.clear();
if (handshakeDataChannel) {
handshakeDataChannel.onopen = null;
handshakeDataChannel.onerror = null;
handshakeDataChannel.onclose = null;
handshakeDataChannel.onbufferedamountlow = null;
handshakeDataChannel.onmessage = null;
}
handshakeDataChannel = undefined;
pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
};
// Function that configures a newly-opened channel and adds it to the map. Used for both
// inbound and outbound substreams.
const addChannel = (dataChannel, direction) => {
const dataChannelId = dataChannel.id;
dataChannel.binaryType = 'arraybuffer';
let isOpen = false;
dataChannel.onopen = () => {
console.assert(!isOpen, "substream opened twice");
isOpen = true;
if (direction === 'first-outbound') {
console.assert(dataChannels.size === 0, "dataChannels not empty when opening");
console.assert(handshakeDataChannel === dataChannel, "handshake substream mismatch");
config.onOpen({
type: 'multi-stream',
handshake: 'webrtc',
// `addChannel` can never be called before the local certificate is generated, so this
// value is always defined.
localTlsCertificateMultihash: localTlsCertificateMultihash,
remoteTlsCertificateMultihash: remoteCertMultihash
});
}
else {
console.assert(direction !== 'outbound' || !handshakeDataChannel, "handshakeDataChannel still defined");
config.onStreamOpened(dataChannelId, direction, 65536);
}
};
dataChannel.onerror = dataChannel.onclose = (_error) => {
// A couple of different things could be happening here.
if (handshakeDataChannel === dataChannel && !isOpen) {
// The handshake data channel that we have opened ahead of time failed to open. As this
// happens before we have reported the WebRTC connection as a whole as being open, we
// need to report that the connection has failed to open.
killAllJs();
// Note that the event doesn't give any additional reason for the failure.
config.onConnectionReset("handshake data channel failed to open");
}
else if (handshakeDataChannel === dataChannel) {
// The handshake data channel has been closed before we reported it to smoldot. This
// isn't really a problem. We just update the state and continue running. If smoldot
// requests a substream, another one will be opened. It could be a valid implementation
// to also just kill the entire connection, however doing so is a bit too intrusive and
// punches through abstraction layers.
handshakeDataChannel.onopen = null;
handshakeDataChannel.onerror = null;
handshakeDataChannel.onclose = null;
handshakeDataChannel.onbufferedamountlow = null;
handshakeDataChannel.onmessage = null;
handshakeDataChannel = undefined;
}
else if (!isOpen) {
// Substream wasn't opened yet and thus has failed to open. The API has no mechanism to
// report substream openings failures. We could try opening it again, but given that
// it's unlikely to succeed, we simply opt to kill the entire connection.
killAllJs();
// Note that the event doesn't give any additional reason for the failure.
config.onConnectionReset("data channel failed to open");
}
else {
// Substream was open and is now closed. Normal situation.
config.onStreamReset(dataChannelId);
}
};
dataChannel.onbufferedamountlow = () => {
const channel = dataChannels.get(dataChannelId);
const val = channel.bufferedBytes;
channel.bufferedBytes = 0;
config.onWritableBytes(val, dataChannelId);
};
dataChannel.onmessage = (m) => {
// The `data` field is an `ArrayBuffer`.
config.onMessage(new Uint8Array(m.data), dataChannelId);
};
if (direction !== 'first-outbound')
dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 });
else
handshakeDataChannel = dataChannel;
};
// It is possible for the browser to use multiple different certificates.
// In order for our local certificate to be deterministic, we need to generate it manually and
// set it explicitly as part of the configuration.
// According to <https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate>,
// browsers are guaranteed to support `{ name: "ECDSA", namedCurve: "P-256" }`.
RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () {
if (pc === null)
return;
// Create a new WebRTC connection.
pc = new RTCPeerConnection({ certificates: [localCertificate] });
// We need to build the multihash corresponding to the local certificate.
// While there exists a `RTCPeerConnection.getFingerprints` function, Firefox notably
// doesn't support it.
// See <https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate#browser_compatibility>
// An alternative to `getFingerprints` is to ask the browser to generate an SDP offer and
// extract from fingerprint from it. Because we explicitly provide a certificate, we have
// the guarantee that the list of certificates will always be the same whenever an SDP offer
// is generated by the browser. However, while this alternative does work on Firefox, it
// doesn't on Chrome, as the SDP offer is for some reason missing the fingerprints.
// Therefore, our strategy is to use `getFingerprints` when it is available (i.e. every
// browser except Firefox), and parse the SDP offer when it is not (i.e. Firefox). In the
// future, only `getFingerprints` would be used.
let localTlsCertificateHex;
if (localCertificate.getFingerprints) {
for (const { algorithm, value } of localCertificate.getFingerprints()) {
if (algorithm === 'sha-256') {
localTlsCertificateHex = value;
break;
}
}
}
else {
const localSdpOffer = yield pc.createOffer();
// Note that this regex is not strict. The browser isn't a malicious actor, and the
// objective of this regex is not to detect invalid input.
const localSdpOfferFingerprintMatch = localSdpOffer.sdp.match(/a(\s*)=(\s*)fingerprint:(\s*)(sha|SHA)-256(\s*)(([a-fA-F0-9]{2}(:)*){32})/);
if (localSdpOfferFingerprintMatch) {
localTlsCertificateHex = localSdpOfferFingerprintMatch[6];
}
}
if (localTlsCertificateHex === undefined) {
// Because we've already returned from the `connect` function at this point, we pretend
// that the connection has failed to open.
config.onConnectionReset('Failed to obtain the browser certificate fingerprint');
return;
}
localTlsCertificateMultihash = new Uint8Array(34);
localTlsCertificateMultihash.set([0x12, 32], 0);
localTlsCertificateMultihash.set(localTlsCertificateHex.split(':').map((s) => parseInt(s, 16)), 2);
// `onconnectionstatechange` is used to detect when the connection has closed or has failed
// to open.
// Note that smoldot will think that the connection is open even when it is still opening.
// Therefore we don't care about events concerning the fact that the connection is now fully
// open.
pc.onconnectionstatechange = (_event) => {
if (pc.connectionState == "closed" || pc.connectionState == "disconnected" || pc.connectionState == "failed") {
killAllJs();
config.onConnectionReset("WebRTC state transitioned to " + pc.connectionState);
}
};
pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () {
var _a;
// Create a new offer and set it as local description.
let sdpOffer = (yield pc.createOffer()).sdp;
// We check that the locally-generated SDP offer has a data channel with the UDP
// protocol. If that isn't the case, the connection will likely fail.
if (sdpOffer.match(/^m=application(\s+)(\d+)(\s+)UDP\/DTLS\/SCTP(\s+)webrtc-datachannel$/m) === null) {
console.error("Local offer doesn't contain UDP data channel. WebRTC connections will likely fail. Please report this issue.");
}
// According to the libp2p WebRTC spec, the ufrag and pwd are the same
// randomly-generated string on both sides, and must be prefixed with
// `libp2p-webrtc-v1:`. We modify the local description to ensure that.
// While we could randomly generate a new string, we just grab the one that the
// browser has generated, in order to make sure that it respects the constraints
// of the ICE protocol.
const browserGeneratedPwd = (_a = sdpOffer.match(/^a=ice-pwd:(.+)$/m)) === null || _a === void 0 ? void 0 : _a.at(1);
if (browserGeneratedPwd === undefined) {
console.error("Failed to set ufrag to pwd. WebRTC connections will likely fail. Please report this issue.");
}
const ufragPwd = "libp2p+webrtc+v1/" + browserGeneratedPwd;
sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd);
sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd);
yield pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
// Transform certificate hash into fingerprint (upper-hex; each byte separated by ":").
const fingerprint = Array.from(remoteCertSha256Hash).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':');
// Note that the trailing line feed is important, as otherwise Chrome
// fails to parse the payload.
const remoteSdp =
// Version of the SDP protocol. Always 0. (RFC8866)
"v=0" + "\n" +
// Identifies the creator of the SDP document. We are allowed to use dummy values
// (`-` and `0.0.0.0`) to remain anonymous, which we do. Note that "IN" means
// "Internet" (and not "input"). (RFC8866)
"o=- 0 0 IN IP" + ipVersion + " " + targetIp + "\n" +
// Name for the session. We are allowed to pass a dummy `-`. (RFC8866)
"s=-" + "\n" +
// Start and end of the validity of the session. `0 0` means that the session never
// expires. (RFC8866)
"t=0 0" + "\n" +
// A lite implementation is only appropriate for devices that will
// always be connected to the public Internet and have a public
// IP address at which it can receive packets from any
// correspondent. ICE will not function when a lite implementation
// is placed behind a NAT (RFC8445).
"a=ice-lite" + "\n" +
// A `m=` line describes a request to establish a certain protocol.
// The protocol in this line (i.e. `TCP/DTLS/SCTP` or `UDP/DTLS/SCTP`) must always be
// the same as the one in the offer. We know that this is true because checked above.
// The `<fmt>` component must always be `webrtc-datachannel` for WebRTC.
// The rest of the SDP payload adds attributes to this specific media stream.
// RFCs: 8839, 8866, 8841
"m=application " + targetPort + " " + "UDP/DTLS/SCTP webrtc-datachannel" + "\n" +
// Indicates the IP address of the remote.
// Note that "IN" means "Internet" (and not "input").
"c=IN IP" + ipVersion + " " + targetIp + "\n" +
// Media ID - uniquely identifies this media stream (RFC9143).
"a=mid:0" + "\n" +
// Indicates that we are complying with RFC8839 (as oppposed to the legacy RFC5245).
"a=ice-options:ice2" + "\n" +
// ICE username and password, which are used for establishing and
// maintaining the ICE connection. (RFC8839)
// These values are set according to the libp2p WebRTC specification.
"a=ice-ufrag:" + ufragPwd + "\n" +
"a=ice-pwd:" + ufragPwd + "\n" +
// Fingerprint of the certificate that the server will use during the TLS
// handshake. (RFC8122)
// MUST be derived from the certificate used by the answerer (server).
"a=fingerprint:sha-256 " + fingerprint + "\n" +
// Indicates that the remote DTLS server will only listen for incoming
// connections. (RFC5763)
// The answerer (server) MUST not be located behind a NAT (RFC6135).
"a=setup:passive" + "\n" +
// The SCTP port (RFC8841)
// Note it's different from the "m=" line port value, which
// indicates the port of the underlying transport-layer protocol
// (UDP or TCP)
"a=sctp-port:5000" + "\n" +
// The maximum SCTP user message size (in bytes) (RFC8841)
// Setting this field is part of the libp2p spec.
"a=max-message-size:16384" + "\n" +
// A transport address for a candidate that can be used for connectivity
// checks (RFC8839).
"a=candidate:1 1 UDP 1 " + targetIp + " " + targetPort + " typ host" + "\n";
yield pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });
});
pc.ondatachannel = ({ channel }) => {
// TODO: is the substream maybe already open? according to the Internet it seems that no but it's unclear
addChannel(channel, 'inbound');
};
// Creating a `RTCPeerConnection` doesn't actually do anything before `createDataChannel` is
// called. Smoldot's API, however, requires you to treat entire connections as open or
// closed. We know, according to the libp2p WebRTC specification, that every connection
// always starts with a substream where a handshake is performed. After we've reported that
// the connection is open, smoldot will open a substream in order to perform the handshake.
// Instead of following this API, we open this substream in advance, and will notify smoldot
// that the connection is open when the substream is open.
// Note that the label passed to `createDataChannel` is required to be empty as per the
// libp2p WebRTC specification.
addChannel(pc.createDataChannel("", { id: 0, negotiated: true }), 'first-outbound');
}));
return {
reset: (streamId) => {
// If `streamId` is undefined, then the whole connection must be destroyed.
if (streamId === undefined) {
killAllJs();
}
else {
const channel = dataChannels.get(streamId);
channel.channel.onopen = null;
channel.channel.onerror = null;
channel.channel.onclose = null;
channel.channel.onbufferedamountlow = null;
channel.channel.onmessage = null;
channel.channel.close();
dataChannels.delete(streamId);
}
},
send: (data, streamId) => {
const channel = dataChannels.get(streamId);
channel.channel.send(data);
channel.bufferedBytes += data.length;
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => {
// `openOutSubstream` can only be called after we have called `config.onOpen`, therefore
// `pc` is guaranteed to be non-null.
// As explained above, we open a data channel ahead of time. If this data channel is still
// there, we report it.
if (handshakeDataChannel) {
// Do this asynchronously because calling callbacks within callbacks is error-prone.
(() => __awaiter(this, void 0, void 0, function* () {
// We need to check again if `handshakeDataChannel` is still defined, as the
// connection might have been closed.
if (handshakeDataChannel) {
config.onStreamOpened(handshakeDataChannel.id, 'outbound', 1024 * 1024);
dataChannels.set(handshakeDataChannel.id, { channel: handshakeDataChannel, bufferedBytes: 0 });
handshakeDataChannel = undefined;
}
}))();
}
else {
// Note that the label passed to `createDataChannel` is required to be empty as per the
// libp2p WebRTC specification.
addChannel(pc.createDataChannel(""), 'outbound');
}
}
};
}
else {
throw new ConnectionError('Unrecognized multiaddr format');
}
}
/// Parses a multihash-multibase-encoded string into a SHA256 hash.
///
/// Throws an exception if the multihash algorithm isn't SHA256.
const multihashToSha256 = (certMultihash) => {
if (certMultihash.length != 34 || certMultihash[0] != 0x12 || certMultihash[1] != 32) {
throw new Error('Certificate multihash is not SHA-256');
}
return new Uint8Array(certMultihash.slice(2));
};

@@ -1,3 +0,3 @@

import { Client, ClientOptions } from './client.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js';
import { Client, ClientOptions } from './public-types.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js';
/**

@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

// Smoldot
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-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());
});
};
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { start as innerStart } from './client.js';
import { ConnectionError } from './instance/instance.js';
import { default as wasmBase64 } from './instance/autogen/wasm.js';
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './client.js';
import { startWithBytecode } from './no-auto-bytecode-deno.js';
import { compileBytecode } from './bytecode-deno.js';
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './public-types.js';
/**

@@ -36,256 +16,3 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

options = options || {};
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
const wasmModule = zlibInflate(trustedBase64Decode(wasmBase64)).then(((bytecode) => WebAssembly.compile(bytecode)));
return innerStart(options || {}, wasmModule, {
registerShouldPeriodicallyYield: (_callback) => {
return [true, () => { }];
},
performanceNow: () => {
return performance.now();
},
getRandomValues: (buffer) => {
const crypto = globalThis.crypto;
if (!crypto)
throw new Error('randomness not available');
crypto.getRandomValues(buffer);
},
connect: (config) => {
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false);
}
});
return startWithBytecode(Object.assign({ bytecode: compileBytecode() }, options));
}
/**
* Applies the zlib inflate algorithm on the buffer.
*/
function zlibInflate(buffer) {
return __awaiter(this, void 0, void 0, function* () {
// This code has been copy-pasted from the official streams draft specification.
// At the moment, it is found here: https://wicg.github.io/compression/#example-deflate-compress
const ds = new DecompressionStream('deflate');
const writer = ds.writable.getWriter();
writer.write(buffer);
writer.close();
const output = [];
const reader = ds.readable.getReader();
let totalSize = 0;
while (true) {
const { value, done } = yield reader.read();
if (done)
break;
output.push(value);
totalSize += value.byteLength;
}
const concatenated = new Uint8Array(totalSize);
let offset = 0;
for (const array of output) {
concatenated.set(array, offset);
offset += array.byteLength;
}
return concatenated;
});
}
/**
* Decodes a base64 string.
*
* The input is assumed to be correct.
*/
function trustedBase64Decode(base64) {
// This code is a bit sketchy due to the fact that we decode into a string, but it seems to
// work.
const binaryString = atob(base64);
const size = binaryString.length;
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
/**
* Tries to open a new connection using the given configuration.
*
* @see Connection
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
*/
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
// Attempt to parse the multiaddress.
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/);
if (wsParsed != null) {
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
if ((proto == 'ws' && forbidWs) ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
(proto == 'wss' && forbidWss)) {
throw new ConnectionError('Connection type not allowed');
}
const url = (wsParsed[1] == 'ip6') ?
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
const socket = new WebSocket(url);
socket.binaryType = 'arraybuffer';
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
const checkBufferedAmount = () => {
if (socket.readyState != 1)
return;
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
// of all the data that has been sent, but that might not be the case. For this
// reason, we use `bufferedAmount` as a hint rather than a correct value.
const bufferedAmount = socket.bufferedAmount;
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
if (wasSent < 0)
wasSent = 0;
bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
bufferedAmountCheck.nextTimeout *= 2;
if (bufferedAmountCheck.nextTimeout > 500)
bufferedAmountCheck.nextTimeout = 500;
}
// Note: it is important to call `onWritableBytes` at the very end, as it might
// trigger a call to `send`.
if (wasSent != 0)
config.onWritableBytes(wasSent);
};
socket.onopen = () => {
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false });
};
socket.onclose = (event) => {
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
config.onConnectionReset(message);
};
socket.onmessage = (msg) => {
config.onMessage(new Uint8Array(msg.data));
};
return {
reset: () => {
// We can't set these fields to null because the TypeScript definitions don't
// allow it, but we can set them to dummy values.
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
socket.close();
},
send: (data) => {
// The WebSocket library that we use seems to spontaneously transition connections
// to the "closed" state but not call the `onclosed` callback immediately. Calling
// `send` on that object throws an exception. In order to avoid panicking smoldot,
// we thus absorb any exception thrown here.
// See also <https://github.com/paritytech/smoldot/issues/2937>.
try {
socket.send(data);
if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
bufferedAmountCheck.nextTimeout = 10;
setTimeout(checkBufferedAmount, 10);
}
bufferedAmountCheck.quenedUnreportedBytes += data.length;
}
catch (_error) { }
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else if (tcpParsed != null) {
if (forbidTcp) {
throw new ConnectionError('TCP connections not available');
}
const socket = {
destroyed: false,
inner: Deno.connect({
hostname: tcpParsed[2],
port: parseInt(tcpParsed[3], 10),
}).catch((error) => {
socket.destroyed = true;
config.onConnectionReset(error.toString());
return null;
})
};
socket.inner = socket.inner.then((established) => {
// TODO: at the time of writing of this comment, `setNoDelay` is still unstable
//established.setNoDelay();
if (socket.destroyed)
return established;
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: true });
// Spawns an asynchronous task that continuously reads from the socket.
// Every time data is read, the task re-executes itself in order to continue reading.
// The task ends automatically if an EOF or error is detected, which should also happen
// if the user calls `close()`.
const read = (readBuffer) => __awaiter(this, void 0, void 0, function* () {
if (socket.destroyed || established === null)
return;
let outcome = null;
try {
outcome = yield established.read(readBuffer);
}
catch (error) {
// The type of `error` is unclear, but we assume that it implements `Error`
outcome = error.toString();
}
if (socket.destroyed)
return;
if (typeof outcome !== 'number' || outcome === null) {
// The socket is reported closed, but `socket.destroyed` is still `false` (see
// check above). As such, we must inform the inner layers.
socket.destroyed = true;
config.onConnectionReset(outcome === null ? "EOF when reading socket" : outcome);
return;
}
console.assert(outcome !== 0); // `read` guarantees to return a non-zero value.
config.onMessage(readBuffer.slice(0, outcome));
return read(readBuffer);
});
read(new Uint8Array(32768));
return established;
});
return {
reset: () => {
socket.destroyed = true;
socket.inner.then((connec) => connec.close());
},
send: (data) => {
let dataCopy = Uint8Array.from(data); // Deep copy of the data
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
while (dataCopy.length > 0) {
if (socket.destroyed || c === null)
return c;
let outcome;
try {
outcome = yield c.write(dataCopy);
config.onWritableBytes(dataCopy.length);
}
catch (error) {
// The type of `error` is unclear, but we assume that it implements `Error`
outcome = error.toString();
}
if (typeof outcome !== 'number') {
// The socket is reported closed, but `socket.destroyed` is still
// `false` (see check above). As such, we must inform the inner layers.
socket.destroyed = true;
config.onConnectionReset(outcome);
return c;
}
// Note that, contrary to `read`, it is possible for `outcome` to be 0.
// This happen if the write had to be interrupted, and the only thing
// we have to do is try writing again.
dataCopy = dataCopy.slice(outcome);
}
return c;
}));
},
closeSend: () => {
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
yield (c === null || c === void 0 ? void 0 : c.closeWrite());
return c;
}));
},
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else {
throw new ConnectionError('Unrecognized multiaddr format');
}
}

@@ -1,3 +0,3 @@

import { Client, ClientOptions } from './client.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js';
import { Client, ClientOptions } from './public-types.js';
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js';
/**

@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

// Smoldot
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Note: if you modify these imports, please test both the ModuleJS and CommonJS generated
// bindings. JavaScript being JavaScript, some libraries (such as `websocket`) have issues working
// with both at the same time.
import { start as innerStart } from './client.js';
import { ConnectionError } from './instance/instance.js';
import { default as wasmBase64 } from './instance/autogen/wasm.js';
import { WebSocket } from 'ws';
import { inflate } from 'pako';
import { performance } from 'node:perf_hooks';
import { createConnection as nodeCreateConnection } from 'node:net';
import { randomFillSync } from 'node:crypto';
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './client.js';
import { startWithBytecode } from './no-auto-bytecode-nodejs.js';
import { compileBytecode } from './bytecode-nodejs.js';
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './public-types.js';
/**

@@ -35,185 +16,3 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain.

options = options || {};
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
const wasmModule = WebAssembly.compile(inflate(Buffer.from(wasmBase64, 'base64')));
return innerStart(options || {}, wasmModule, {
registerShouldPeriodicallyYield: (_callback) => {
return [true, () => { }];
},
performanceNow: () => {
return performance.now();
},
getRandomValues: (buffer) => {
if (buffer.length >= 1024 * 1024)
throw new Error('getRandomValues buffer too large');
randomFillSync(buffer);
},
connect: (config) => {
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false);
}
});
return startWithBytecode(Object.assign({ bytecode: compileBytecode() }, options));
}
/**
* Tries to open a new connection using the given configuration.
*
* @see Connection
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
*/
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
// Attempt to parse the multiaddress.
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/);
if (wsParsed != null) {
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
if ((proto == 'ws' && forbidWs) ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
(proto == 'wss' && forbidWss)) {
throw new ConnectionError('Connection type not allowed');
}
const url = (wsParsed[1] == 'ip6') ?
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
const socket = new WebSocket(url);
socket.binaryType = 'arraybuffer';
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
const checkBufferedAmount = () => {
if (socket.readyState != 1)
return;
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
// of all the data that has been sent, but that seems to not be the case. It is
// unclear whether this is intended or a bug, but is is likely that `bufferedAmount`
// also includes WebSocket headers. For this reason, we use `bufferedAmount` as a hint
// rather than a correct value.
const bufferedAmount = socket.bufferedAmount;
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
if (wasSent < 0)
wasSent = 0;
bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
bufferedAmountCheck.nextTimeout *= 2;
if (bufferedAmountCheck.nextTimeout > 500)
bufferedAmountCheck.nextTimeout = 500;
}
// Note: it is important to call `onWritableBytes` at the very end, as it might
// trigger a call to `send`.
if (wasSent != 0)
config.onWritableBytes(wasSent);
};
socket.onopen = () => {
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false });
};
socket.onclose = (event) => {
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
config.onConnectionReset(message);
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
};
socket.onerror = (event) => {
config.onConnectionReset(event.message);
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
};
socket.onmessage = (msg) => {
config.onMessage(new Uint8Array(msg.data));
};
return {
reset: () => {
// We can't set these fields to null because the TypeScript definitions don't
// allow it, but we can set them to dummy values.
socket.onopen = () => { };
socket.onclose = () => { };
socket.onmessage = () => { };
socket.onerror = () => { };
socket.close();
},
send: (data) => {
socket.send(data);
if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
bufferedAmountCheck.nextTimeout = 10;
setTimeout(checkBufferedAmount, 10);
}
bufferedAmountCheck.quenedUnreportedBytes += data.length;
},
closeSend: () => { throw new Error('Wrong connection type'); },
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else if (tcpParsed != null) {
// `net` module will be missing when we're not in NodeJS.
if (forbidTcp) {
throw new ConnectionError('TCP connections not available');
}
const socket = nodeCreateConnection({
host: tcpParsed[2],
port: parseInt(tcpParsed[3], 10),
});
// Number of bytes queued using `socket.write` and where `write` has returned false.
const drainingBytes = { num: 0 };
socket.setNoDelay();
socket.on('connect', () => {
if (socket.destroyed)
return;
config.onOpen({
type: 'single-stream', handshake: 'multistream-select-noise-yamux',
initialWritableBytes: socket.writableHighWaterMark, writeClosable: true
});
});
socket.on('close', (hasError) => {
if (socket.destroyed)
return;
// NodeJS doesn't provide a reason why the closing happened, but only
// whether it was caused by an error.
const message = hasError ? "Error" : "Closed gracefully";
config.onConnectionReset(message);
});
socket.on('error', () => { });
socket.on('data', (message) => {
if (socket.destroyed)
return;
config.onMessage(new Uint8Array(message.buffer));
});
socket.on('drain', () => {
// The bytes queued using `socket.write` and where `write` has returned false have now
// been sent. Notify the API that it can write more data.
if (socket.destroyed)
return;
const val = drainingBytes.num;
drainingBytes.num = 0;
config.onWritableBytes(val);
});
return {
reset: () => {
socket.destroy();
},
send: (data) => {
const dataLen = data.length;
const allWritten = socket.write(data);
if (allWritten) {
setImmediate(() => {
if (!socket.writable)
return;
config.onWritableBytes(dataLen);
});
}
else {
drainingBytes.num += dataLen;
}
},
closeSend: () => {
socket.end();
},
openOutSubstream: () => { throw new Error('Wrong connection type'); }
};
}
else {
throw new ConnectionError('Unrecognized multiaddr format');
}
}
{
"name": "smoldot",
"version": "1.0.4",
"version": "1.0.5",
"description": "Light client that connects to Polkadot and Substrate-based blockchains",

@@ -22,9 +22,41 @@ "author": "Parity Technologies <admin@parity.io>",

"exports": {
"node": {
"import": "./dist/mjs/index-nodejs.js",
"require": "./dist/cjs/index-nodejs.js"
".": {
"node": {
"import": "./dist/mjs/index-nodejs.js",
"require": "./dist/cjs/index-nodejs.js"
},
"default": {
"import": "./dist/mjs/index-browser.js",
"require": "./dist/cjs/index-browser.js"
}
},
"default": {
"import": "./dist/mjs/index-browser.js",
"require": "./dist/cjs/index-browser.js"
"./no-auto-bytecode": {
"node": {
"import": "./dist/mjs/no-auto-bytecode-nodejs.js",
"require": "./dist/cjs/no-auto-bytecode-nodejs.js"
},
"default": {
"import": "./dist/mjs/no-auto-bytecode-browser.js",
"require": "./dist/cjs/no-auto-bytecode-browser.js"
}
},
"./worker": {
"node": {
"import": "./dist/mjs/worker-nodejs.js",
"require": "./dist/cjs/worker-nodejs.js"
},
"default": {
"import": "./dist/mjs/worker-browser.js",
"require": "./dist/cjs/worker-browser.js"
}
},
"./bytecode": {
"node": {
"import": "./dist/mjs/bytecode-nodejs.js",
"require": "./dist/cjs/bytecode-nodejs.js"
},
"default": {
"import": "./dist/mjs/bytecode-browser.js",
"require": "./dist/cjs/bytecode-browser.js"
}
}

@@ -31,0 +63,0 @@ },

@@ -70,1 +70,59 @@ # Light client for Polkadot and Substrate-based chains

sandbox".
## Usage with a worker
By default, calling `start()` will run smoldot entirely in the current thread. This can cause
performance issues if other CPU-heavy operations are done in that thread.
In order to help with this, it possible to use smoldot in conjunction with a worker.
To do so, you must first create a worker. Since creating a worker has some subtle differences
depending on the platform, this is outside of the responsibility of smoldot.
Once the worker is created, create two `MessagePort`s using `new MessageChannel`, and send one
of them to the worker. Then, pass one port to the `ClientOptions.portToWorker` field and the
other port to the `run()` function of smoldot, which can be imported with
`import { run } from 'smoldot/worker';` (on Deno, it is found in `worker-deno.ts`).
Another optimization that is orthogonal to but is related to running smoldot in a worker consists
in also loading the smoldot bytecode in that worker. The smoldot bytecode weights several
megabytes, and loading it in a worker rather than the main thread makes it possible to load the
UI while smoldot is still initializing. This is especially important when smoldot is included in
an application served over the web.
In order to load the smoldot bytecode in a worker, import `compileBytecode` with
`import { compileBytecode } from 'smoldot/bytecode';` (on Deno: `bytecode-deno.ts`), then call the
function and send the result to the main thread. From the main thread, rather than using the
`start` function imported from `smoldot`, use the `startWithBytecode` function that can be imported
using `import { startWithBytecode } from 'smoldot/no-auto-bytecode';` (on Deno:
`no-auto-bytecode-deno.ts`). The options provided to `startWithBytecode` are the same as the ones
passed to `start`, except for an additional `bytecode` field that must be set to the bytecode
created in the worker.
Here is an example of all this, assuming a browser environment:
```ts
import * as smoldot from 'smoldot/no-auto-bytecode';
const worker = new Worker(new URL('./worker.js', import.meta.url));
const bytecode = new Promise((resolve) => {
worker.onmessage = (event) => resolve(event.data);
});
const { port1, port2 } = new MessageChannel();
worker.postMessage(port1, [port1]);
const client = smoldot.startWithBytecode({
bytecode,
portToWorker: port2,
});
// `worker.ts`
import * as smoldot from '@substrate/smoldot-light/worker';
import { compileBytecode } from '@substrate/smoldot-light/bytecode';
compileBytecode().then((bytecode) => postMessage(bytecode))
onmessage = (msg) => smoldot.run(msg.data);
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc