archethic
Version:
Archethic Javascript SDK
286 lines (237 loc) • 7.71 kB
JavaScript
import fetch from "cross-fetch";
import { uint8ArrayToHex } from "./utils.js";
import absinthe from "./api/absinthe.cjs";
const senderContext = "SENDER";
export default class TransactionSender {
constructor() {
this.onSent = [];
this.onConfirmation = [];
this.onFullConfirmation = [];
this.onRequiredConfirmation = [];
this.onError = [];
this.onTimeout = [];
this.confirmationNotifier = undefined;
this.errorNotifier = undefined;
this.absintheSocket = undefined;
this.timeout = undefined;
this.nbConfirmationReceived = 0;
return this;
}
/**
* Add listener on specific event
* @param {String} event Event to subscribe
* @param {Function} func Function to call when event triggered
*/
on(event, func) {
if (typeof event !== "string") {
throw "'event' must be a string";
}
if (typeof func !== "function") {
throw "'func' must be a function";
}
switch (event) {
case "sent":
this.onSent.push(func);
break;
case "confirmation":
this.onConfirmation.push(func);
break;
case "requiredConfirmation":
this.onRequiredConfirmation.push(func);
break;
case "fullConfirmation":
this.onFullConfirmation.push(func);
break;
case "error":
this.onError.push(func);
break;
case "timeout":
this.onTimeout.push(func);
break;
default:
throw "Event " + event + " is not supported";
}
return this;
}
async send(tx, endpoint, confirmationThreshold = 100, timeout = 60) {
if (typeof tx !== "object") {
throw "'tx' must be an instance of TransactionBuilder";
}
if (typeof endpoint !== "string") {
throw "'endpoint' must be a string";
}
if (
typeof confirmationThreshold !== "number" ||
(confirmationThreshold < 0 && confirmationThreshold > 100)
) {
throw "'confirmationThreshold' must be an integer between 0 and 100";
}
if (typeof timeout !== "number" || timeout <= 0) {
throw "'timeout' must be an integer greater than 0";
}
const txAddress = uint8ArrayToHex(tx.address);
// Create web socket
const { host, protocol } = new URL(endpoint);
const ws_protocol = protocol == "https:" ? "wss" : "ws";
this.absintheSocket = absinthe.create(`${ws_protocol}://${host}/socket`);
try {
this.confirmationNotifier = await waitConfirmations(
txAddress,
this.absintheSocket,
(nbConf, maxConf) =>
handleConfirmation.call(this, confirmationThreshold, nbConf, maxConf)
);
this.errorNotifier = await waitError(
txAddress,
this.absintheSocket,
handleError.bind(this)
);
} catch (err) {
this.onError.forEach((func) => func(senderContext, err.message, this));
return this;
}
// Send transaction
fetch(endpoint + "/api/transaction", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: tx.toJSON(),
})
.then((response) => handleSend.call(this, timeout, response))
.catch((err) =>
this.onError.forEach((func) => func(senderContext, err, this))
);
return this;
}
unsubscribe(event = undefined) {
if (event) {
switch (event) {
case "sent":
this.onSent = [];
break;
case "confirmation":
this.onConfirmation = [];
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
break;
case "requiredConfirmation":
this.onRequiredConfirmation = [];
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
break;
case "fullConfirmation":
this.onFullConfirmation = [];
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
break;
case "error":
this.onError = [];
absinthe.cancel(this.absintheSocket, this.errorNotifier);
break;
case "timeout":
this.onTimeout = [];
break;
default:
throw "Event " + event + " is not supported";
}
} else {
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
absinthe.cancel(this.absintheSocket, this.errorNotifier);
this.onConfirmation = [];
this.onFullConfirmation = [];
this.onRequiredConfirmation = [];
this.onError = [];
this.onTimeout = [];
this.onSent = [];
}
}
}
async function waitConfirmations(address, absintheSocket, handler) {
const operation = `
subscription {
transactionConfirmed(address: "${address}") {
nbConfirmations,
maxConfirmations
}
}
`;
const notifier = absinthe.send(absintheSocket, operation);
return absinthe.observe(absintheSocket, notifier, (result) => {
if (result.data.transactionConfirmed) {
const {
nbConfirmations: nbConfirmations,
maxConfirmations: maxConfirmations,
} = result.data.transactionConfirmed;
handler(nbConfirmations, maxConfirmations);
}
});
}
async function waitError(address, absintheSocket, handler) {
const operation = `
subscription {
transactionError(address: "${address}") {
context,
reason
}
}
`;
const notifier = absinthe.send(absintheSocket, operation);
return absinthe.observe(absintheSocket, notifier, (result) => {
if (result.data.transactionError) {
const { context: context, reason: reason } = result.data.transactionError;
handler(context, reason);
}
});
}
function handleConfirmation(
confirmationThreshold,
nbConfirmations,
maxConfirmations
) {
// Update nb confirmation received for timeout
this.nbConfirmationReceived = nbConfirmations;
// Unsubscribe to error on first confirmation
if (nbConfirmations == 1)
absinthe.cancel(this.absintheSocket, this.errorNotifier);
this.onConfirmation.forEach((func) =>
func(nbConfirmations, maxConfirmations, this)
);
if (
maxConfirmations * (confirmationThreshold / 100) <= nbConfirmations &&
this.onRequiredConfirmation.length > 0
) {
this.onRequiredConfirmation.forEach((func) => func(nbConfirmations, this));
this.onRequiredConfirmation = [];
clearTimeout(this.timeout);
}
if (nbConfirmations == maxConfirmations) {
clearTimeout(this.timeout);
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
this.onFullConfirmation.forEach((func) => func(maxConfirmations, this));
}
}
function handleError(context, reason) {
clearTimeout(this.timeout);
// Unsubscribe to all subscriptions
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
absinthe.cancel(this.absintheSocket, this.errorNotifier);
this.onError.forEach((func) => func(context, reason, this));
}
function handleSend(timeout, response) {
if (response.status >= 200 && response.status <= 299) {
this.onSent.forEach((func) => func(this));
// Setup 1 minute timeout
this.timeout = setTimeout(() => {
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
absinthe.cancel(this.absintheSocket, this.errorNotifier);
this.onTimeout.forEach((func) => func(this.nbConfirmationReceived, this));
}, timeout * 1_000);
} else {
absinthe.cancel(this.absintheSocket, this.confirmationNotifier);
absinthe.cancel(this.absintheSocket, this.errorNotifier);
response
.json()
.then((err) =>
this.onError.forEach((func) => func(senderContext, err.status, this))
);
}
}