UNPKG

@logsn/arweave

Version:
309 lines (308 loc) 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const error_1 = require("./lib/error"); const transaction_1 = require("./lib/transaction"); const ArweaveUtils = require("./lib/utils"); const transaction_uploader_1 = require("./lib/transaction-uploader"); require("arconnect"); class Transactions { api; crypto; chunks; constructor(api, crypto, chunks) { this.api = api; this.crypto = crypto; this.chunks = chunks; } async getTransactionAnchor() { const res = await this.api.get(`tx_anchor`); if (!res.data.match(/^[a-z0-9_-]{43,}/i) || !res.ok) { throw new Error(`Could not getTransactionAnchor. Received: ${res.data}. Status: ${res.status}, ${res.statusText}`); } return res.data; } async getPrice(byteSize, targetAddress) { let endpoint = targetAddress ? `price/${byteSize}/${targetAddress}` : `price/${byteSize}`; const res = await this.api.get(endpoint); if (!/^\d+$/.test(res.data) || !res.ok) { throw new Error(`Could not getPrice. Received: ${res.data}. Status: ${res.status}, ${res.statusText}`); } return res.data; } async get(id) { const response = await this.api.get(`tx/${id}`); if (response.status == 200) { const data_size = parseInt(response.data.data_size); if (response.data.format >= 2 && data_size > 0 && data_size <= 1024 * 1024 * 12) { const data = await this.getData(id); return new transaction_1.default({ ...response.data, data, }); } return new transaction_1.default({ ...response.data, format: response.data.format || 1, }); } if (response.status == 404) { throw new error_1.default("TX_NOT_FOUND" /* ArweaveErrorType.TX_NOT_FOUND */); } if (response.status == 410) { throw new error_1.default("TX_FAILED" /* ArweaveErrorType.TX_FAILED */); } throw new error_1.default("TX_INVALID" /* ArweaveErrorType.TX_INVALID */); } fromRaw(attributes) { return new transaction_1.default(attributes); } async search(tagName, tagValue) { return this.api .post(`arql`, { op: "equals", expr1: tagName, expr2: tagValue, }) .then((response) => { if (!response.data) { return []; } return response.data; }); } getStatus(id) { return this.api.get(`tx/${id}/status`).then((response) => { if (response.status == 200) { return { status: 200, confirmed: response.data, }; } return { status: response.status, confirmed: null, }; }); } async getData(id, options) { let data = undefined; try { data = await this.chunks.downloadChunkedData(id); } catch (error) { console.error(`Error while trying to download chunked data for ${id}`); console.error(error); } if (!data) { console.warn(`Falling back to gateway cache for ${id}`); try { const { data: resData, ok, status, statusText, } = await this.api.get(`/${id}`, { responseType: "arraybuffer" }); if (!ok) { throw new Error(`Bad http status code`, { cause: { status, statusText }, }); } data = resData; } catch (error) { console.error(`Error while trying to download contiguous data from gateway cache for ${id}`); console.error(error); } } if (!data) { throw new Error(`${id} data was not found!`); } if (options && options.decode && !options.string) { return data; } if (options && options.decode && options.string) { return ArweaveUtils.bufferToString(data); } // Since decode wasn't requested, caller expects b64url encoded data. return ArweaveUtils.bufferTob64Url(data); } async sign(transaction, jwk, //"use_wallet" for backwards compatibility only options) { /** Non-exhaustive (only checks key names), but previously no jwk checking was done */ const isJwk = (obj) => { let valid = true; ["n", "e", "d", "p", "q", "dp", "dq", "qi"].map((key) => !(key in obj) && (valid = false)); return valid; }; const validJwk = typeof jwk === "object" && isJwk(jwk); const externalWallet = typeof arweaveWallet === "object"; if (!validJwk && !externalWallet) { throw new Error(`No valid JWK or external wallet found to sign transaction.`); } else if (validJwk) { transaction.setOwner(jwk.n); let dataToSign = await transaction.getSignatureData(); let rawSignature = await this.crypto.sign(jwk, dataToSign, options); let id = await this.crypto.hash(rawSignature); transaction.setSignature({ id: ArweaveUtils.bufferTob64Url(id), owner: jwk.n, signature: ArweaveUtils.bufferTob64Url(rawSignature), }); } else if (externalWallet) { try { const existingPermissions = await arweaveWallet.getPermissions(); if (!existingPermissions.includes("SIGN_TRANSACTION")) await arweaveWallet.connect(["SIGN_TRANSACTION"]); } catch { // Permission is already granted } const signedTransaction = await arweaveWallet.sign(transaction, options); transaction.setSignature({ id: signedTransaction.id, owner: signedTransaction.owner, reward: signedTransaction.reward, tags: signedTransaction.tags, signature: signedTransaction.signature, }); } else { //can't get here, but for sanity we'll throw an error. throw new Error(`An error occurred while signing. Check wallet is valid`); } } async verify(transaction) { const signaturePayload = await transaction.getSignatureData(); /** * The transaction ID should be a SHA-256 hash of the raw signature bytes, so this needs * to be recalculated from the signature and checked against the transaction ID. */ const rawSignature = transaction.get("signature", { decode: true, string: false, }); const expectedId = ArweaveUtils.bufferTob64Url(await this.crypto.hash(rawSignature)); if (transaction.id !== expectedId) { throw new Error(`Invalid transaction signature or ID! The transaction ID doesn't match the expected SHA-256 hash of the signature.`); } /** * Now verify the signature is valid and signed by the owner wallet (owner field = originating wallet public key). */ return this.crypto.verify(transaction.owner, signaturePayload, rawSignature); } async post(transaction) { if (typeof transaction === "string") { transaction = new transaction_1.default(JSON.parse(transaction)); } else if (typeof transaction.readInt32BE === "function") { transaction = new transaction_1.default(JSON.parse(transaction.toString())); } else if (typeof transaction === "object" && !(transaction instanceof transaction_1.default)) { transaction = new transaction_1.default(transaction); } if (!(transaction instanceof transaction_1.default)) { throw new Error(`Must be Transaction object`); } if (!transaction.chunks) { await transaction.prepareChunks(transaction.data); } const uploader = await this.getUploader(transaction, transaction.data); // Emulate existing error & return value behavior. try { while (!uploader.isComplete) { await uploader.uploadChunk(); } } catch (e) { if (uploader.lastResponseStatus > 0) { return { status: uploader.lastResponseStatus, statusText: uploader.lastResponseError, data: { error: uploader.lastResponseError, }, }; } throw e; } return { status: 200, statusText: "OK", data: {}, }; } /** * Gets an uploader than can be used to upload a transaction chunk by chunk, giving progress * and the ability to resume. * * Usage example: * * ``` * const uploader = arweave.transactions.getUploader(transaction); * while (!uploader.isComplete) { * await uploader.uploadChunk(); * console.log(`${uploader.pctComplete}%`); * } * ``` * * @param upload a Transaction object, a previously save progress object, or a transaction id. * @param data the data of the transaction. Required when resuming an upload. */ async getUploader(upload, data) { let uploader; if (data instanceof ArrayBuffer) { data = new Uint8Array(data); } if (upload instanceof transaction_1.default) { if (!data) { data = upload.data; } if (!(data instanceof Uint8Array)) { throw new Error("Data format is invalid"); } if (!upload.chunks) { await upload.prepareChunks(data); } uploader = new transaction_uploader_1.TransactionUploader(this.api, upload); if (!uploader.data || uploader.data.length === 0) { uploader.data = data; } } else { if (typeof upload === "string") { upload = await transaction_uploader_1.TransactionUploader.fromTransactionId(this.api, upload); } if (!data || !(data instanceof Uint8Array)) { throw new Error(`Must provide data when resuming upload`); } // upload should be a serialized upload. uploader = await transaction_uploader_1.TransactionUploader.fromSerialized(this.api, upload, data); } return uploader; } /** * Async generator version of uploader * * Usage example: * * ``` * for await (const uploader of arweave.transactions.upload(tx)) { * console.log(`${uploader.pctComplete}%`); * } * ``` * * @param upload a Transaction object, a previously save uploader, or a transaction id. * @param data the data of the transaction. Required when resuming an upload. */ async *upload(upload, data) { const uploader = await this.getUploader(upload, data); while (!uploader.isComplete) { await uploader.uploadChunk(); yield uploader; } return uploader; } } exports.default = Transactions;