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.
61 lines
No EOL
7.1 KiB
JSON
61 lines
No EOL
7.1 KiB
JSON
{
|
|
"contractName": "UsdListing",
|
|
"constructorInputs": [
|
|
{ "name": "oraclePk", "type": "pubkey" },
|
|
{ "name": "sellerPkh", "type": "bytes20" },
|
|
{ "name": "targetCents", "type": "int" },
|
|
{ "name": "floorSats", "type": "int" },
|
|
{ "name": "ceilSats", "type": "int" },
|
|
{ "name": "maxAgeSecs", "type": "int" }
|
|
],
|
|
"abi": [
|
|
{
|
|
"name": "buy",
|
|
"inputs": [
|
|
{ "name": "oracleMsg", "type": "bytes" },
|
|
{ "name": "oracleSig", "type": "datasig" }
|
|
]
|
|
},
|
|
{
|
|
"name": "cancel",
|
|
"inputs": [
|
|
{ "name": "pk", "type": "pubkey" },
|
|
{ "name": "s", "type": "sig" }
|
|
]
|
|
}
|
|
],
|
|
"bytecode": "OP_6 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_TXLOCKTIME OP_CHECKLOCKTIMEVERIFY OP_DROP OP_7 OP_PICK OP_SIZE OP_NIP 28 OP_NUMEQUALVERIFY OP_8 OP_ROLL OP_8 OP_PICK OP_ROT OP_CHECKDATASIGVERIFY OP_6 OP_ROLL OP_4 OP_SPLIT OP_4 OP_SPLIT OP_ROT OP_BIN2NUM OP_ROT OP_BIN2NUM OP_DUP OP_0 OP_GREATERTHAN OP_VERIFY OP_ROT OP_INPUTINDEX OP_OUTPOINTTXHASH OP_EQUALVERIFY OP_TXLOCKTIME 302a OP_ADD OP_2 OP_PICK OP_GREATERTHANOREQUAL OP_VERIFY OP_TXLOCKTIME OP_ROT OP_7 OP_ROLL OP_ADD OP_LESSTHANOREQUAL OP_VERIFY OP_ROT 00e1f505 OP_MUL OP_SWAP OP_DIV OP_DUP OP_3 OP_PICK OP_LESSTHAN OP_IF OP_2 OP_PICK OP_NIP OP_ENDIF OP_DUP OP_4 OP_PICK OP_GREATERTHAN OP_IF OP_3 OP_PICK OP_NIP OP_ENDIF OP_0 OP_OUTPUTBYTECODE 76a914 OP_3 OP_ROLL OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUAL OP_NIP OP_NIP OP_NIP OP_ELSE OP_6 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_6 OP_PICK OP_HASH160 OP_ROT OP_EQUALVERIFY OP_6 OP_ROLL OP_6 OP_ROLL OP_CHECKSIGVERIFY OP_2DROP OP_2DROP OP_DROP OP_1 OP_ENDIF",
|
|
"source": "pragma cashscript ^0.13.0;\n\n// USD-pegged sale of a BCNR name certificate (CashTokens NFT).\n//\n// The certificate sits in this covenant while it is for sale. Anyone may\n// buy it by paying the seller the BCH equivalent of `targetCents` at the\n// oracle's current price; the seller may reclaim it at any time.\n//\n// Oracle message (40 bytes): timestamp as uint32 LE, price in USD cents per\n// BCH as uint32 LE, then the 32-byte txid of the listing's own outpoint,\n// signed by `oraclePk` (Schnorr data signature over the sha256 of the\n// message, as checkDataSig expects).\n//\n// What the chain can and cannot enforce: script can require a transaction\n// to be mined no EARLIER than a time (nLocktime), never no later, so a quote\n// cannot expire on-chain. The [floorSats, ceilSats] band the seller chooses\n// is therefore the real guarantee: whatever quote a buyer presents, the\n// payout stays inside it. Binding the quote to the listing's outpoint means\n// cancelling and relisting voids every quote ever issued. The locktime rule\n// below still stops a quote from being used before the oracle issued it.\ncontract UsdListing(\n pubkey oraclePk,\n bytes20 sellerPkh,\n int targetCents,\n int floorSats,\n int ceilSats,\n int maxAgeSecs\n) {\n // Buyer path. Output 0 pays the seller; output 1 carries the certificate\n // to wherever the buyer wants it. Remaining outputs are the buyer's.\n function buy(bytes oracleMsg, datasig oracleSig) {\n require(oracleMsg.length == 40);\n require(checkDataSig(oracleSig, oracleMsg, oraclePk));\n\n bytes tsBytes, bytes rest = oracleMsg.split(4);\n bytes priceBytes, bytes quotedTxid = rest.split(4);\n int ts = int(tsBytes);\n int priceCents = int(priceBytes);\n require(priceCents > 0);\n require(quotedTxid == tx.inputs[this.activeInputIndex].outpointTransactionHash);\n\n // Not before the quote: nLocktime is checked against median-time-past\n // (about an hour behind the clock), so the buyer sets it a couple of\n // hours behind the quote and the contract allows that slack.\n require(tx.locktime + 10800 >= ts);\n require(tx.locktime <= ts + maxAgeSecs);\n\n int need = targetCents * 100000000 / priceCents;\n if (need < floorSats) { need = floorSats; }\n if (need > ceilSats) { need = ceilSats; }\n\n // Seller is paid first, in plain BCH.\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(sellerPkh));\n require(tx.outputs[0].value >= need);\n require(tx.outputs[0].tokenCategory == 0x);\n\n // The certificate leaves intact: same category, same commitment.\n require(tx.outputs[1].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[1].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n }\n\n // Seller path: reclaim the certificate (cancel the listing, or re-list\n // at a new target).\n function cancel(pubkey pk, sig s) {\n require(hash160(pk) == sellerPkh);\n require(checkSig(s, pk));\n }\n}\n",
|
|
"fingerprint": "b4c2176de988b48f6a69c7c93fd3fa5ca03f6fee2e5e6201aa765e9af4c0e01b",
|
|
"debug": {
|
|
"bytecode": "5679009c63c5b1755779827701289d587a58797bbb567a547f547f7b817b817600a0697bc0c888c502302a935279a269c57b577a93a1697b0400e1f505957c967653799f6352797768765479a0635379776800cd0376a914537a7e0288ac7e8800cca16900d1008851d1c0ce8851d2c0cf8777777767567a519d5679a97b88567a567aad6d6d755168",
|
|
"sourceMap": "31:4:60:5;;;;;:53:31:53;::::1;;32:16:32:25:0;;:::32:1;;:36::38:0;:8::40:1;33:29:33:38:0;;:40::49;;:51::59;:8::62:1;35:36:35:45:0;;:52::53;:36::54:1;36:56:36:57:0;:45::58:1;37:21:37:28:0;:17::29:1;38:29:38:39:0;:25::40:1;39:16:39:26:0;:29::30;:16:::1;:8::32;40:16:40:26:0;:40::61;:30::86:1;:8::88;45:16:45:27:0;:30::35;:16:::1;:39::41:0;;:16:::1;:8::43;46:16:46:27:0;:31::33;:36::46;;:31:::1;:16;:8::48;48:19:48:30:0;:33::42;:19:::1;:45::55:0;:19:::1;49:12:49:16:0;:19::28;;:12:::1;:30::51:0;:39::48;;:32::49:1;:30::51;50:12:50:16:0;:19::27;;:12:::1;:29::49:0;:38::46;;:31::47:1;:29::49;53:27:53:28:0;:16::45:1;:49::84:0;:74::83;;:49::84:1;;;:8::86;54:27:54:28:0;:16::35:1;:::43;:8::45;55:27:55:28:0;:16::43:1;:47::49:0;:8::51:1;58:27:58:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;59:27:59:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;31:53:60:5;;;:4;64::67::0;;;;65:24:65:26;;:16::27:1;:31::40:0;:8::42:1;66:25:66:26:0;;:28::30;;:8::33:1;64:38:67:5;;;;21:0:68:1",
|
|
"logs": [],
|
|
"requires": [
|
|
{ "ip": 12, "line": 31, "message": "Using tx.locktime requires a non-final sequence number on the spending input" },
|
|
{ "ip": 19, "line": 32 },
|
|
{ "ip": 25, "line": 33 },
|
|
{ "ip": 39, "line": 39 },
|
|
{ "ip": 43, "line": 40 },
|
|
{ "ip": 50, "line": 45 },
|
|
{ "ip": 57, "line": 46 },
|
|
{ "ip": 89, "line": 53 },
|
|
{ "ip": 93, "line": 54 },
|
|
{ "ip": 97, "line": 55 },
|
|
{ "ip": 102, "line": 58 },
|
|
{ "ip": 108, "line": 59 },
|
|
{ "ip": 120, "line": 65 },
|
|
{ "ip": 125, "line": 66 }
|
|
],
|
|
"sourceTags": "5:7:lg;102:104:sc"
|
|
},
|
|
"compiler": {
|
|
"name": "cashc",
|
|
"version": "0.13.3",
|
|
"options": {
|
|
"enforceFunctionParameterTypes": true,
|
|
"enforceLocktimeGuard": true
|
|
}
|
|
},
|
|
"updatedAt": "2026-09-20T17:10:46.988Z"
|
|
} |