Security
This commit is contained in:
+17
-5
@@ -105,6 +105,12 @@ export class Market {
|
||||
snapshot.transfers.filter(tx => transactionCardIds(tx).includes(hash)).length === sequence &&
|
||||
snapshot.blocks.length + 1 - block.height >= CONSENSUS.coinbaseMaturity;
|
||||
}
|
||||
private deliveryId(order: MarketOrder): string {
|
||||
// Identity excludes signature bytes. Recognize exact sale delivery through ANY submission route,
|
||||
// including peer gossip or a direct /api/transfer, even before the market records delivery intent.
|
||||
return transactionId({ network: NETWORK, chainId: CHAIN_ID, cardHash: order.cardHash,
|
||||
from: order.seller, to: order.buyer, sequence: order.sequence, publicKey: '', signature: '' });
|
||||
}
|
||||
/** Reconcile from canonical transaction identity, never from a mempool acknowledgement. */
|
||||
private reconcile() {
|
||||
const s = this.chain.snapshot(), now = this.now();
|
||||
@@ -112,7 +118,8 @@ export class Market {
|
||||
for (const o of this.rows<MarketOrder>('orders')) {
|
||||
const l = this.listing(o.listingId);
|
||||
if (o.status === 'refunded' || o.status === 'expired') continue;
|
||||
const delivery = o.transferId && s.blocks.find(b => b.transactions.some(tx => transactionId(tx) === o.transferId));
|
||||
const deliveryId = this.deliveryId(o);
|
||||
const delivery = s.blocks.find(b => b.transactions.some(tx => transactionId(tx) === deliveryId));
|
||||
const confirmations = delivery ? s.blocks.length - delivery.height + 1 : 0;
|
||||
o.deliveryConfirmations = confirmations;
|
||||
if (o.status === 'complete' || o.status === 'review') {
|
||||
@@ -122,7 +129,12 @@ export class Market {
|
||||
}
|
||||
this.saveOrder(o); continue; // Never automatically pay a second time after a reorganization.
|
||||
}
|
||||
if (delivery) {
|
||||
if (delivery && o.status === 'awaiting-payment') {
|
||||
// A gift/direct delivery is not evidence that payment was received. Never pay an unpaid order.
|
||||
o.status = 'review'; o.payout = 'review';
|
||||
if (o.additionalFee) o.additionalFee.payout = 'review';
|
||||
} else if (delivery) {
|
||||
o.transferId = deliveryId;
|
||||
o.status = 'delivery-pending';
|
||||
if (confirmations >= o.requiredConfirmations) {
|
||||
o.status = 'complete'; o.payout = 'mock-paid'; l.status = 'sold';
|
||||
@@ -131,8 +143,8 @@ export class Market {
|
||||
} else if (o.status === 'awaiting-payment' && now >= o.payBy) {
|
||||
o.status = 'expired'; l.status = l.kind === 'fixed' ? 'open' : 'expired';
|
||||
} else if (o.status !== 'awaiting-payment') {
|
||||
const pending = s.pendingTransfers.some(tx => transactionId(tx) === o.transferId);
|
||||
const conflict = s.pendingTransfers.some(tx => transactionCardIds(tx).includes(o.cardHash) && transactionId(tx) !== o.transferId);
|
||||
const pending = s.pendingTransfers.some(tx => transactionId(tx) === deliveryId);
|
||||
const conflict = s.pendingTransfers.some(tx => transactionCardIds(tx).includes(o.cardHash) && transactionId(tx) !== deliveryId);
|
||||
if (!this.card(s, o.cardHash, o.seller, o.sequence) || conflict || (now >= o.deliverBy && !pending)) o.status = 'refund-required';
|
||||
else if (o.status === 'delivery-pending' && !pending) o.status = 'paid';
|
||||
}
|
||||
@@ -252,7 +264,7 @@ export class Market {
|
||||
o.status = 'paid'; o.deliverBy = now + DELIVERY_WINDOW;
|
||||
} else {
|
||||
if (o.status !== 'refund-required' || s.pendingTransfers.some(tx => transactionCardIds(tx).includes(o.cardHash)) ||
|
||||
s.blocks.some(b => b.transactions.some(tx => transactionId(tx) === o.transferId))) throw new Error('Refund requires failed delivery and no pending/canonical sale transfer');
|
||||
s.blocks.some(b => b.transactions.some(tx => transactionId(tx) === this.deliveryId(o)))) throw new Error('Refund requires failed delivery and no pending/canonical sale transfer');
|
||||
o.status = 'refunded'; const l = this.listing(o.listingId); l.status = 'cancelled'; this.saveListing(l);
|
||||
}
|
||||
this.saveOrder(o); return o;
|
||||
|
||||
Reference in New Issue
Block a user