@logsn/arweave
Version:
Arweave JS client library
224 lines (223 loc) • 8.78 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionUploader = void 0;
const transaction_1 = require("./transaction");
const ArweaveUtils = require("./utils");
const error_1 = require("./error");
const merkle_1 = require("./merkle");
// Maximum amount of chunks we will upload in the body.
const MAX_CHUNKS_IN_BODY = 1;
// We assume these errors are intermitment and we can try again after a delay:
// - not_joined
// - timeout
// - data_root_not_found (we may have hit a node that just hasn't seen it yet)
// - exceeds_disk_pool_size_limit
// We also try again after any kind of unexpected network errors
// Errors from /chunk we should never try and continue on.
const FATAL_CHUNK_UPLOAD_ERRORS = [
"invalid_json",
"chunk_too_big",
"data_path_too_big",
"offset_too_big",
"data_size_too_big",
"chunk_proof_ratio_not_attractive",
"invalid_proof",
];
// Amount we will delay on receiving an error response but do want to continue.
const ERROR_DELAY = 1000 * 40;
class TransactionUploader {
api;
chunkIndex = 0;
txPosted = false;
transaction;
lastRequestTimeEnd = 0;
totalErrors = 0; // Not serialized.
data;
lastResponseStatus = 0;
lastResponseError = "";
get isComplete() {
return (this.txPosted &&
this.chunkIndex === this.transaction.chunks.chunks.length);
}
get totalChunks() {
return this.transaction.chunks.chunks.length;
}
get uploadedChunks() {
return this.chunkIndex;
}
get pctComplete() {
return Math.trunc((this.uploadedChunks / this.totalChunks) * 100);
}
constructor(api, transaction) {
this.api = api;
if (!transaction.id) {
throw new Error(`Transaction is not signed`);
}
if (!transaction.chunks) {
throw new Error(`Transaction chunks not prepared`);
}
// Make a copy of transaction, zeroing the data so we can serialize.
this.data = transaction.data;
this.transaction = new transaction_1.default(Object.assign({}, transaction, { data: new Uint8Array(0) }));
}
/**
* Uploads the next part of the transaction.
* On the first call this posts the transaction
* itself and on any subsequent calls uploads the
* next chunk until it completes.
*/
async uploadChunk(chunkIndex_) {
if (this.isComplete) {
throw new Error(`Upload is already complete`);
}
if (this.lastResponseError !== "") {
this.totalErrors++;
}
else {
this.totalErrors = 0;
}
// We have been trying for about an hour receiving an
// error every time, so eventually bail.
if (this.totalErrors === 100) {
throw new Error(`Unable to complete upload: ${this.lastResponseStatus}: ${this.lastResponseError}`);
}
let delay = this.lastResponseError === ""
? 0
: Math.max(this.lastRequestTimeEnd + ERROR_DELAY - Date.now(), ERROR_DELAY);
if (delay > 0) {
// Jitter delay bcoz networks, subtract up to 30% from 40 seconds
delay = delay - delay * Math.random() * 0.3;
await new Promise((res) => setTimeout(res, delay));
}
this.lastResponseError = "";
if (!this.txPosted) {
await this.postTransaction();
return;
}
if (chunkIndex_) {
this.chunkIndex = chunkIndex_;
}
const chunk = this.transaction.getChunk(chunkIndex_ || this.chunkIndex, this.data);
const chunkOk = await (0, merkle_1.validatePath)(this.transaction.chunks.data_root, parseInt(chunk.offset), 0, parseInt(chunk.data_size), ArweaveUtils.b64UrlToBuffer(chunk.data_path));
if (!chunkOk) {
throw new Error(`Unable to validate chunk ${this.chunkIndex}`);
}
// Catch network errors and turn them into objects with status -1 and an error message.
const resp = await this.api
.post(`chunk`, this.transaction.getChunk(this.chunkIndex, this.data))
.catch((e) => {
console.error(e.message);
return { status: -1, data: { error: e.message } };
});
this.lastRequestTimeEnd = Date.now();
this.lastResponseStatus = resp.status;
if (this.lastResponseStatus == 200) {
this.chunkIndex++;
}
else {
this.lastResponseError = (0, error_1.getError)(resp);
if (FATAL_CHUNK_UPLOAD_ERRORS.includes(this.lastResponseError)) {
throw new Error(`Fatal error uploading chunk ${this.chunkIndex}: ${this.lastResponseError}`);
}
}
}
/**
* Reconstructs an upload from its serialized state and data.
* Checks if data matches the expected data_root.
*
* @param serialized
* @param data
*/
static async fromSerialized(api, serialized, data) {
if (!serialized ||
typeof serialized.chunkIndex !== "number" ||
typeof serialized.transaction !== "object") {
throw new Error(`Serialized object does not match expected format.`);
}
// Everything looks ok, reconstruct the TransactionUpload,
// prepare the chunks again and verify the data_root matches
var transaction = new transaction_1.default(serialized.transaction);
if (!transaction.chunks) {
await transaction.prepareChunks(data);
}
const upload = new TransactionUploader(api, transaction);
// Copy the serialized upload information, and data passed in.
upload.chunkIndex = serialized.chunkIndex;
upload.lastRequestTimeEnd = serialized.lastRequestTimeEnd;
upload.lastResponseError = serialized.lastResponseError;
upload.lastResponseStatus = serialized.lastResponseStatus;
upload.txPosted = serialized.txPosted;
upload.data = data;
if (upload.transaction.data_root !== serialized.transaction.data_root) {
throw new Error(`Data mismatch: Uploader doesn't match provided data.`);
}
return upload;
}
/**
* Reconstruct an upload from the tx metadata, ie /tx/<id>.
*
* @param api
* @param id
* @param data
*/
static async fromTransactionId(api, id) {
const resp = await api.get(`tx/${id}`);
if (resp.status !== 200) {
throw new Error(`Tx ${id} not found: ${resp.status}`);
}
const transaction = resp.data;
transaction.data = new Uint8Array(0);
const serialized = {
txPosted: true,
chunkIndex: 0,
lastResponseError: "",
lastRequestTimeEnd: 0,
lastResponseStatus: 0,
transaction,
};
return serialized;
}
toJSON() {
return {
chunkIndex: this.chunkIndex,
transaction: this.transaction,
lastRequestTimeEnd: this.lastRequestTimeEnd,
lastResponseStatus: this.lastResponseStatus,
lastResponseError: this.lastResponseError,
txPosted: this.txPosted,
};
}
// POST to /tx
async postTransaction() {
const uploadInBody = this.totalChunks <= MAX_CHUNKS_IN_BODY;
if (uploadInBody) {
// Post the transaction with data.
this.transaction.data = this.data;
const resp = await this.api.post(`tx`, this.transaction).catch((e) => {
console.error(e);
return { status: -1, data: { error: e.message } };
});
this.lastRequestTimeEnd = Date.now();
this.lastResponseStatus = resp.status;
this.transaction.data = new Uint8Array(0);
if (resp.status >= 200 && resp.status < 300) {
// We are complete.
this.txPosted = true;
this.chunkIndex = MAX_CHUNKS_IN_BODY;
return;
}
this.lastResponseError = (0, error_1.getError)(resp);
throw new Error(`Unable to upload transaction: ${resp.status}, ${this.lastResponseError}`);
}
// Post the transaction with no data.
const resp = await this.api.post(`tx`, this.transaction);
this.lastRequestTimeEnd = Date.now();
this.lastResponseStatus = resp.status;
if (!(resp.status >= 200 && resp.status < 300)) {
this.lastResponseError = (0, error_1.getError)(resp);
throw new Error(`Unable to upload transaction: ${resp.status}, ${this.lastResponseError}`);
}
this.txPosted = true;
}
}
exports.TransactionUploader = TransactionUploader;