diff --git a/.vscode/settings.json b/.vscode/settings.json index e13a40e..704a271 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,15 @@ { "mode": "auto" } + ], + "cSpell.words": [ + "bitcoinjs", + "fastify", + "ipfs", + "psbt", + "sado", + "txid", + "utxo", + "vout" ] -} \ No newline at end of file +} diff --git a/src/JsonRpc/Errors.ts b/src/JsonRpc/Errors.ts index ed894b8..1ac2eef 100644 --- a/src/JsonRpc/Errors.ts +++ b/src/JsonRpc/Errors.ts @@ -1,5 +1,21 @@ import { RpcError } from "./Core"; +/** + * The **HTTP 400 Bad Request** response status code indicates that the server + * cannot or will not process the request due to something that is perceived to + * be a client error. + */ +export class BadRequestError extends RpcError { + /** + * Instantiate a new BadRequestError. + * + * @param data - Optional data to send with the error. + */ + constructor(message = "Bad Request", data?: D) { + super(message, -32000, data); + } +} + /** * The **HTTP 401 Unauthorized** response status code indicates that the client * request has not been completed because it lacks valid authentication @@ -21,8 +37,8 @@ export class UnauthorizedError extends RpcError { * * @param data - Optional data to send with the error. */ - constructor(data?: D) { - super("Unauthorized", -32001, data); + constructor(message = "Unauthorized", data?: D) { + super(message, -32001, data); } } @@ -40,10 +56,11 @@ export class ForbiddenError extends RpcError { /** * Instantiate a new ForbiddenError. * + * @param message - Optional message to send with the error. Default: "Forbidden". * @param data - Optional data to send with the error. */ - constructor(data?: D) { - super("Forbidden", -32003, data); + constructor(message = "Forbidden", data?: D) { + super(message, -32003, data); } } @@ -79,9 +96,10 @@ export class NotAcceptableError extends RpcError { /** * Instantiate a new NotAcceptableError. * - * @param data - Optional data to send with the error. + * @param message - Optional message to send with the error. Default: "Not Acceptable". + * @param data - Optional data to send with the error. */ - constructor(message: string, data?: D) { + constructor(message = "Not Acceptable", data?: D) { super(message, -32006, data); } } @@ -100,9 +118,10 @@ export class ConflictError extends RpcError { /** * Instantiate a new ConflictError. * + * @param message - Optional message to send with the error. Default: "Conflict". * @param data - Optional data to send with the error. */ - constructor(data?: D) { - super("Conflict", -32009, data); + constructor(message = "Conflict", data?: D) { + super(message, -32009, data); } } diff --git a/src/Methods/Offer.ts b/src/Methods/Offer.ts new file mode 100644 index 0000000..62adb38 --- /dev/null +++ b/src/Methods/Offer.ts @@ -0,0 +1,26 @@ +import { api } from "../Api"; +import { BadRequestError, method } from "../JsonRpc"; +import { utils } from "../Orderbook/Utilities"; + +api.register< + { + offer: string; + }, + { + type: string; + offer: any; + } +>( + "DecodeOffer", + method(async ({ offer }) => { + const psbt = utils.psbt.decode(offer); + if (psbt !== undefined) { + return { type: "psbt", offer: psbt }; + } + const tx = utils.raw.decode(offer); + if (tx !== undefined) { + return { type: "raw", offer: tx }; + } + throw new BadRequestError(); + }) +); diff --git a/src/Orderbook/Utilities/Raw.ts b/src/Orderbook/Utilities/Raw.ts new file mode 100644 index 0000000..28a36ee --- /dev/null +++ b/src/Orderbook/Utilities/Raw.ts @@ -0,0 +1,20 @@ +import * as btc from "bitcoinjs-lib"; + +export const raw = { + decode, +}; + +/** + * Attempt to retrieve a raw unsigned transaction from the offer string. + * + * @param offer - Encoded offer transaction. + * + * @returns The raw tx or undefined if it could not be parsed. + */ +function decode(offer: string): any | undefined { + try { + return btc.Transaction.fromHex(offer); + } catch (error) { + return undefined; + } +} diff --git a/src/Orderbook/Utilities/index.ts b/src/Orderbook/Utilities/index.ts index 62b14b7..12eddf3 100644 --- a/src/Orderbook/Utilities/index.ts +++ b/src/Orderbook/Utilities/index.ts @@ -5,9 +5,11 @@ import { PriceList } from "../../Libraries/PriceList"; import { parseLocation } from "../../Libraries/Transaction"; import { Lookup } from "../../Services/Lookup"; import { psbt } from "./PSBT"; +import { raw } from "./Raw"; export const utils = { psbt, + raw, }; /** diff --git a/src/Orderbook/Validator/Offer/index.ts b/src/Orderbook/Validator/Offer/index.ts index a6b6eaf..531ba39 100644 --- a/src/Orderbook/Validator/Offer/index.ts +++ b/src/Orderbook/Validator/Offer/index.ts @@ -26,21 +26,28 @@ async function hasValidOrder(cid: string): Promise { async function hasValidOffer({ offer }: IPFSOffer, order: IPFSOrder, lookup: Lookup): Promise { const psbt = utils.psbt.decode(offer); - if (psbt === undefined) { - return validateRawTx(offer); + if (psbt !== undefined) { + await validateMakerInput(psbt, order.location); + await validateTransactionInputs(psbt, lookup); + return; + } + const raw = validateRawTx(offer); + if (raw === false) { + throw new OfferValidationFailed("Unable to verify offer validity", { offer }); } - await validateMakerInput(psbt, order.location); - await validateTransactionInputs(psbt, lookup); } async function validateMakerInput(psbt: btc.Psbt, location: string): Promise { const [txid, index] = parseLocation(location); - const hasMakerInput = hasOrderInput(psbt, txid, index); - if (hasMakerInput === false) { + const vinIndex = hasOrderInput(psbt, txid, index); + if (vinIndex === false) { throw new OfferValidationFailed("Offer vin does not include the location specified in the order", { location, }); } + if (vinIndex !== 0) { + throw new OfferValidationFailed("Offer location is not the first vin of the transaction", { location }); + } } async function validateTransactionInputs(psbt: btc.Psbt, lookup: Lookup): Promise { @@ -61,12 +68,13 @@ async function validateTransactionInputs(psbt: btc.Psbt, lookup: Lookup): Promis } } -function hasOrderInput(psbt: btc.Psbt, txid: string, vout: number): boolean { +function hasOrderInput(psbt: btc.Psbt, txid: string, vout: number): number | false { for (const input of psbt.data.inputs) { if (input.nonWitnessUtxo) { const tx = btc.Transaction.fromBuffer(input.nonWitnessUtxo); - if (tx.getId() === txid && tx.outs.findIndex((_, index) => index === vout) !== -1) { - return true; + const index = tx.outs.findIndex((_, index) => index === vout); + if (tx.getId() === txid && index !== -1) { + return index; } } } @@ -88,16 +96,15 @@ function hasOrderInput(psbt: btc.Psbt, txid: string, vout: number): boolean { * * @returns `true` if a signature exists in the inputs of the transaction. */ -export function validateRawTx(offer: string): undefined { - try { - const tx = btc.Transaction.fromHex(offer); - for (const input of tx.ins) { - if (input.script.toString()) { - return; - } +export function validateRawTx(offer: string): boolean { + const tx = utils.raw.decode(offer); + if (tx === undefined) { + throw new OfferValidationFailed("Unable to verify offer validity", { offer }); + } + for (const input of tx.ins) { + if (input.script.toString()) { + return true; } - } catch (error) { - throw new OfferValidationFailed(error.message, { offer }); } - throw new OfferValidationFailed("Unable to verify offer validity", { offer }); + return false; } diff --git a/src/main.ts b/src/main.ts index f80409e..db32da0 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import "./Methods/Orderbook"; import "./Methods/IPFS"; +import "./Methods/Offer"; import debug from "debug";