A BCH-priced offer is a signed transaction with a fixed amount, so a seller who wants "$500" had to relist whenever the rate moved. The new USD mode moves the certificate into a small CashScript covenant (contracts/usd-listing.cash). Anyone may buy it by paying the seller target_cents / price, where the price comes from a message signed by the gateway's oracle key — the median of five exchanges the site already uses. The payout is clamped to a floor/ceiling band the seller sets, and the seller can reclaim the certificate at any time. What the chain can and cannot enforce, stated plainly in the contract and the UI: script can require a transaction to be mined no earlier than a time, never no later, so a quote cannot expire on-chain. The band is the guarantee, and quotes are bound to the listing's outpoint so cancelling and relisting voids every quote ever issued. An offline libauth VM test covers list, buy, underpayment, foreign-listing quotes, forged quotes, band clamping, cancel and a non-seller cancel; on chipnet aloevera.test was listed at $0.30, bought by a second wallet for 119,090 sat at $251.91, listed again by the new owner and cancelled. Gateway: GET /api/price/oracle[?txid=] serves the oracle public key and per-listing signed quotes; the key lives in oracle.key beside the gateway, never in the repo. POST /api/market accepts kind "usd" after reproducing the redeem script from the stated terms and finding the certificate in that covenant; such listings are pruned once the covenant output is spent; DELETE accepts the listing's seller while the index shows the covenant as holder. Dashboard: the Sell tab has "Price is fixed in: US dollars / BCH", a band control with the resulting BCH range, and cancel through the covenant. Market: USD listings show the dollar price, today's BCH equivalent and the band; buying fetches a quote and completes the covenant spend. Studio no longer sends a needless UPD when the registry view is stale, and missing files on a name now get a real 404 page.
68 lines
3 KiB
Text
68 lines
3 KiB
Text
pragma cashscript ^0.13.0;
|
|
|
|
// USD-pegged sale of a BCNR name certificate (CashTokens NFT).
|
|
//
|
|
// The certificate sits in this covenant while it is for sale. Anyone may
|
|
// buy it by paying the seller the BCH equivalent of `targetCents` at the
|
|
// oracle's current price; the seller may reclaim it at any time.
|
|
//
|
|
// Oracle message (40 bytes): timestamp as uint32 LE, price in USD cents per
|
|
// BCH as uint32 LE, then the 32-byte txid of the listing's own outpoint,
|
|
// signed by `oraclePk` (Schnorr data signature over the sha256 of the
|
|
// message, as checkDataSig expects).
|
|
//
|
|
// What the chain can and cannot enforce: script can require a transaction
|
|
// to be mined no EARLIER than a time (nLocktime), never no later, so a quote
|
|
// cannot expire on-chain. The [floorSats, ceilSats] band the seller chooses
|
|
// is therefore the real guarantee: whatever quote a buyer presents, the
|
|
// payout stays inside it. Binding the quote to the listing's outpoint means
|
|
// cancelling and relisting voids every quote ever issued. The locktime rule
|
|
// below still stops a quote from being used before the oracle issued it.
|
|
contract UsdListing(
|
|
pubkey oraclePk,
|
|
bytes20 sellerPkh,
|
|
int targetCents,
|
|
int floorSats,
|
|
int ceilSats,
|
|
int maxAgeSecs
|
|
) {
|
|
// Buyer path. Output 0 pays the seller; output 1 carries the certificate
|
|
// to wherever the buyer wants it. Remaining outputs are the buyer's.
|
|
function buy(bytes oracleMsg, datasig oracleSig) {
|
|
require(oracleMsg.length == 40);
|
|
require(checkDataSig(oracleSig, oracleMsg, oraclePk));
|
|
|
|
bytes tsBytes, bytes rest = oracleMsg.split(4);
|
|
bytes priceBytes, bytes quotedTxid = rest.split(4);
|
|
int ts = int(tsBytes);
|
|
int priceCents = int(priceBytes);
|
|
require(priceCents > 0);
|
|
require(quotedTxid == tx.inputs[this.activeInputIndex].outpointTransactionHash);
|
|
|
|
// Not before the quote: nLocktime is checked against median-time-past
|
|
// (about an hour behind the clock), so the buyer sets it a couple of
|
|
// hours behind the quote and the contract allows that slack.
|
|
require(tx.locktime + 10800 >= ts);
|
|
require(tx.locktime <= ts + maxAgeSecs);
|
|
|
|
int need = targetCents * 100000000 / priceCents;
|
|
if (need < floorSats) { need = floorSats; }
|
|
if (need > ceilSats) { need = ceilSats; }
|
|
|
|
// Seller is paid first, in plain BCH.
|
|
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(sellerPkh));
|
|
require(tx.outputs[0].value >= need);
|
|
require(tx.outputs[0].tokenCategory == 0x);
|
|
|
|
// The certificate leaves intact: same category, same commitment.
|
|
require(tx.outputs[1].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
|
|
require(tx.outputs[1].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
|
|
}
|
|
|
|
// Seller path: reclaim the certificate (cancel the listing, or re-list
|
|
// at a new target).
|
|
function cancel(pubkey pk, sig s) {
|
|
require(hash160(pk) == sellerPkh);
|
|
require(checkSig(s, pk));
|
|
}
|
|
}
|