Skip to content

Commit

Permalink
feat(offer): add offer transaction fee
Browse files Browse the repository at this point in the history
  • Loading branch information
kodemon committed May 18, 2023
1 parent 7fcf4ee commit fb7256a
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 46 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "tsc",
"start": "DEBUG=sado-* ts-node-dev ./src/main.ts",
"clean": "rm -rf dist",
"lint": "eslint --ext .ts src"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@fastify/cors": "^8.2.1",
Expand Down
27 changes: 23 additions & 4 deletions src/Entities/Offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { Network } from "../Libraries/Network";
import { PriceList } from "../Libraries/PriceList";
import { getAddressVoutValue } from "../Libraries/Transaction";
import { IPFSLookupFailed } from "../Orderbook/Exceptions/GeneralExceptions";
import { getAskingPrice } from "../Orderbook/Utilities";
import { getAskingPrice, utils } from "../Orderbook/Utilities";
import { validator } from "../Orderbook/Validator";
import { ipfs } from "../Services/IPFS";
import { Lookup } from "../Services/Lookup";
import { db } from "../Services/Mongo";
import { IPFSOffer, IPFSOrder } from "./IPFS";
import { Inscription, Ordinal, Transaction, Vout } from "./Transaction";
Expand Down Expand Up @@ -59,6 +60,11 @@ export class Offer {
*/
readonly time: OfferTime;

/**
* Estimated fee in satoshis for the offer transaction.
*/
readonly fee?: number;

/**
* Vout containing the ordinals and inscription array.
*/
Expand All @@ -83,6 +89,7 @@ export class Offer {
this.offer = document.offer;
this.value = document.value;
this.time = document.time;
this.fee = document.fee;
this.vout = document.vout;
this.proof = document.proof;
this.rejection = document.rejection;
Expand All @@ -94,7 +101,7 @@ export class Offer {
|--------------------------------------------------------------------------------
*/

static async insert(tx: Transaction): Promise<Offer | undefined> {
static async insert(tx: Transaction, lookup: Lookup): Promise<Offer | undefined> {
const offer = await ipfs.getOffer(tx.cid);
if ("error" in offer) {
await collection.insertOne(makeRejectedOffer(tx, new IPFSLookupFailed(tx.txid, offer.error, offer.data)));
Expand All @@ -105,7 +112,8 @@ export class Offer {
await collection.insertOne(makeRejectedOffer(tx, new IPFSLookupFailed(tx.txid, order.error, order.data), offer));
return;
}
const result = await collection.insertOne(makePendingOffer(tx, order, offer));
const fee = await getTransactioFee(offer.offer, lookup);
const result = await collection.insertOne(makePendingOffer(tx, order, offer, fee));
if (result.acknowledged === true) {
return this.findById(result.insertedId);
}
Expand Down Expand Up @@ -198,6 +206,7 @@ export class Offer {
},
ago: moment(this.tx.blocktime).fromNow(), // [TODO] Deprecate in favor of `time.ago`.
value: new PriceList(this.value),
fee: new PriceList(this.fee),
order: {
...this.order,
price: new PriceList(getAskingPrice(this.order)),
Expand Down Expand Up @@ -231,7 +240,7 @@ export class Offer {
|--------------------------------------------------------------------------------
*/

function makePendingOffer(tx: Transaction, order: IPFSOrder, offer: IPFSOffer): OfferDocument {
function makePendingOffer(tx: Transaction, order: IPFSOrder, offer: IPFSOffer, fee?: number): OfferDocument {
return {
status: "pending",
address: tx.from,
Expand All @@ -243,6 +252,7 @@ function makePendingOffer(tx: Transaction, order: IPFSOrder, offer: IPFSOffer):
block: tx.blocktime,
offer: offer.ts,
},
fee,
tx,
};
}
Expand All @@ -263,6 +273,14 @@ function makeRejectedOffer(tx: Transaction, rejection: any, offer?: IPFSOffer):
};
}

async function getTransactioFee(offer: string, lookup: Lookup): Promise<number | undefined> {
const psbt = utils.psbt.decode(offer);
if (psbt === undefined) {
return undefined;
}
return utils.psbt.getFee(psbt, lookup);
}

/*
|--------------------------------------------------------------------------------
| Document
Expand All @@ -278,6 +296,7 @@ type OfferDocument = {
offer: IPFSOffer;
value?: number;
time: OfferTime;
fee?: number;
vout?: Vout;
proof?: string;
rejection?: any;
Expand Down
11 changes: 11 additions & 0 deletions src/Libraries/Bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import { dexPrices } from "../Services/DexPrices";
*/
export const BTC_TO_SAT = 100_000_000;

/**
* Convert provided BTC to satoshis.
*
* @param btc - BTC to convert.
*
* @returns Satoshi value.
*/
export function btcToSat(btc: number): number {
return btc * BTC_TO_SAT;
}

/**
* Convert provided satoshis to BTC.
*
Expand Down
22 changes: 14 additions & 8 deletions src/Orderbook/Resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ export async function resolveOrderbookTransactions(address: string, network: Net
// ### Add Transactions
// For any non-processed transactions in the address, add them to the database.

const result: (Order | Offer)[] = [];

const nextTxs = await addOrderbookTransactions(sadoTxs, address, network);
const result = await Promise.all(
nextTxs.map((tx) => {
if (tx.type === "order") {
return Order.insert(tx);
for (const tx of nextTxs) {
if (tx.type === "order") {
const order = await Order.insert(tx);
if (order !== undefined) {
result.push(order);
}
if (tx.type === "offer") {
return Offer.insert(tx);
}
if (tx.type === "offer") {
const offer = await Offer.insert(tx, lookup);
if (offer !== undefined) {
result.push(offer);
}
})
);
}
}

// ### Resolve Pending
// Run through pending orders and offers, checking of changes and transitioning
Expand Down
61 changes: 61 additions & 0 deletions src/Orderbook/Utilities/PSBT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as btc from "bitcoinjs-lib";

import { btcToSat } from "../../Libraries/Bitcoin";
import { Lookup } from "../../Services/Lookup";

export const psbt = {
decode,
getFee,
};

/**
* Attempt to retrieve a PSBT from the offer string. We try both hex and base64
* formats as we don't know which one the user will provide.
*
* @param offer - Encoded offer transaction.
*
* @returns The PSBT or undefined if it could not be parsed.
*/
function decode(offer: string): btc.Psbt | undefined {
try {
return btc.Psbt.fromHex(offer);
} catch (err) {
// TODO: Add better check in case the error is not about failure to
// parse the hex.
// not a PSBT hex offer
}
try {
return btc.Psbt.fromBase64(offer);
} catch (err) {
// TODO: Add better check in case the error is not about failure to
// parse the base64.
// not a PSBT base64 offer
}
}

/**
* Calculate the fee for given PSBT by looking up the input transactions and
* subtracting the output values.
*
* @param psbt - The PSBT to calculate the fee for.
* @param lookup - The lookup service to use to retrieve the input transactions.
*
* @returns The fee in satoshis.
*/
async function getFee(psbt: btc.Psbt, lookup: Lookup): Promise<number> {
let inputSum = 0;
for (const input of psbt.txInputs) {
const hash = input.hash.reverse().toString("hex");
const tx = await lookup.getTransaction(hash);
if (tx !== undefined) {
inputSum += btcToSat(tx.vout[input.index].value);
}
}

let outputSum = 0;
for (const output of psbt.txOutputs) {
outputSum += output.value;
}

return inputSum - outputSum;
}
17 changes: 11 additions & 6 deletions src/Orderbook/Utilities.ts → src/Orderbook/Utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { IPFSOffer, IPFSOrder } from "../Entities/IPFS";
import { Transaction } from "../Entities/Transaction";
import { BTC_TO_SAT } from "../Libraries/Bitcoin";
import { PriceList } from "../Libraries/PriceList";
import { parseLocation } from "../Libraries/Transaction";
import { Lookup } from "../Services/Lookup";
import { IPFSOffer, IPFSOrder } from "../../Entities/IPFS";
import { Transaction } from "../../Entities/Transaction";
import { BTC_TO_SAT } from "../../Libraries/Bitcoin";
import { PriceList } from "../../Libraries/PriceList";
import { parseLocation } from "../../Libraries/Transaction";
import { Lookup } from "../../Services/Lookup";
import { psbt } from "./PSBT";

export const utils = {
psbt,
};

/**
* Get order item from a vout scriptPubKey utf8 string.
Expand Down
28 changes: 2 additions & 26 deletions src/Orderbook/Validator/Offer/Format/PSBT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as btc from "bitcoinjs-lib";
import { IPFSOffer, IPFSOrder } from "../../../../Entities/IPFS";
import { parseLocation } from "../../../../Libraries/Transaction";
import { OfferValidationFailed } from "../../../Exceptions/OfferException";
import { utils } from "../../../Utilities";

/**
* Attempt to validate the offer string as a PSBT. If the offer is not a PSBT
Expand All @@ -16,39 +17,14 @@ import { OfferValidationFailed } from "../../../Exceptions/OfferException";
* @returns `true` if the offer is a valid PSBT, `false` otherwise.
*/
export function validatePSBT({ offer }: IPFSOffer, { location }: IPFSOrder): boolean {
const psbt = getPsbt(offer);
const psbt = utils.psbt.decode(offer);
if (psbt === undefined) {
return false;
}
validateOrderInput(psbt, location);
return true;
}

/**
* Attempt to retrieve a PSBT from the offer string. We try both hex and base64
* formats as we don't know which one the user will provide.
*
* @param offer - Encoded offer transaction.
*
* @returns The PSBT or undefined if it could not be parsed.
*/
function getPsbt(offer: string): btc.Psbt | undefined {
try {
return btc.Psbt.fromHex(offer);
} catch (err) {
// TODO: Add better check in case the error is not about failure to
// parse the hex.
// not a PSBT hex offer
}
try {
return btc.Psbt.fromBase64(offer);
} catch (err) {
// TODO: Add better check in case the error is not about failure to
// parse the base64.
// not a PSBT base64 offer
}
}

// ### ORDER INPUTS

function validateOrderInput(psbt: btc.Psbt, location: string): void {
Expand Down
10 changes: 9 additions & 1 deletion src/Services/Lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import debug from "debug";
import fetch, { RequestInit } from "node-fetch";

import { config } from "../Config";
import { getTransaction, Inscription, Ordinal, ScriptPubKey, Transaction } from "../Entities/Transaction";
import {
addTransaction,
getTransaction,
Inscription,
Ordinal,
ScriptPubKey,
Transaction,
} from "../Entities/Transaction";
import { DEFAULT_NETWORK, Network } from "../Libraries/Network";

const log = debug("sado-lookup");
Expand Down Expand Up @@ -60,6 +67,7 @@ export class Lookup {
}
const tx = (await getTransaction(txid, this.network)) ?? (await get("/transaction", { txid }, this.network));
if (tx !== undefined) {
await addTransaction(tx, this.network);
this.transactions.set(txid, tx);
}
return tx;
Expand Down

0 comments on commit fb7256a

Please sign in to comment.