From acf1c762de08811ee5f3d64ba9c970389c5a4321 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:05:23 -0500 Subject: [PATCH 01/16] Update .env.example Updated fee and Unspent API and ORD endpoints Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- .env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index efa3ed1..5a9110e 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,6 @@ NODE_RPC_URL= NODE_RPC_USER= NODE_RPC_PASS= TESTNET=false -FEE_PER_KB=500000000 -UNSPENT_API=https://unspent.dogeord.io/api/v1/address/unspent/ -ORD=https://wonky-ord.dogeord.io/ +FEE_PER_KB=50000000 +UNSPENT_API=https://unspent.dogeord.io/api/v2/address/unspent/ +ORD=https://wonky-ord-v2.dogeord.io/ From 3fa9d629a2a6820829fc38d4bb9fdeab381c648b Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Tue, 24 Dec 2024 18:18:39 -0500 Subject: [PATCH 02/16] Update README.md Lowered Fee, it was set too high Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6560cb..f73a11a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ NODE_RPC_URL=http://: NODE_RPC_USER= NODE_RPC_PASS= TESTNET=false -FEE_PER_KB=500000000 +FEE_PER_KB=50000000 ORD=https://ord.dunesprotocol.com/ ``` From 0de060d919001bc0de010ad2ae28f25273a86954 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:07:49 -0400 Subject: [PATCH 03/16] Updated dunes.js with parent image feature: Added `` to `deployOpenDune` and fixed `getrawtx` function This commit introduces two key updates to the `dunes.js` script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated dunes.js with parent image feature: Added `[parentId]` to `deployOpenDune` and fixed `getrawtx` function This commit introduces two key updates to the `dunes.js` script: 1. **Added `parentId` Parameter to `deployOpenDune`**: - The `deployOpenDune` command now accepts an optional `[parentId]` parameter. This allows users to link a new Dune to a parent inscription by spending its UTXO, enabling hierarchical relationships between inscriptions (inspired by the Doginals protocol). - **Example**: `node dunes.js deployOpenDune "MY•CHILD•DUNE" "Đ" 10 8 null 0 0 0 0 0 true true e6c6efe91b6084eae2c8a2fd6470d3d0dbfbb342f1b8601904f45be8095058e2i0` 2. **Fixed `getrawtx` Function**: - Corrected the `getrawtx` function to properly accept a transaction ID (`txid`) string and pass it to the `getrawtransaction` RPC method, resolving a bug that caused invalid parameter errors. These updates are backward - compatible - existing commands like `printDunes`, `sendDuneMulti`, and `mintDune` will work as before unless the new `parentId` parameter is specified, function is only called by fetchParentUtxo, which is only used when parentId is provided. If you don’t use "Optional" `parentId`, `getrawtx` isn’t even touched, so existing commands (`printDunes`, `sendDuneMulti`, etc.) are unaffected. The script has been tested on the Dogecoin testnet and is ready for mainnet deployment, provided all dependencies and environment variables are correctly configured. This updated script is correct and won’t fuck up the existing dunes.js functionality. The parentId addition is cleanly integrated, only kicking in when used, and the `getrawtx` update fixes a bug without rippling out to other parts. You can deploy Dunes with or without a parent inscription, and everything else like (sending, minting, deploying, syncing) keeps humming along as before. Before, passing tx.toString() was nonsense for an RPC call expecting a txid string. Now, it takes the txid directly (e.g., e6c6...), which is what getrawtransaction needs. The params: [txid, true] ensures verbose output, giving you the full transaction details (like vout data) needed for fetchParentUtxo. Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- dunes.js | 120 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/dunes.js b/dunes.js index e747bf1..ee33fe6 100755 --- a/dunes.js +++ b/dunes.js @@ -865,9 +865,10 @@ function isSingleEmoji(str) { return matches ? matches.length === 1 : false; } +// Updated deployOpenDune command with optional parentId program .command("deployOpenDune") - .description("Deploy a Dune that is open for mint") + .description("Deploy a Dune that is open for mint with an optional parent inscription") .argument("", "Tick for the dune") .argument("", "symbol") .argument("", "Max amount that can be minted in one transaction") @@ -876,22 +877,11 @@ program .argument("", "Absolute block height where minting opens") .argument("", "Absolute block height where minting closes") .argument("", "Relative block height where minting opens") - .argument( - "", - "Relative block height where minting closes (former known as term)" - ) - .argument( - "", - "Amount of allocated dunes to the etcher while etching" - ) - .argument( - "", - "Marks this etching as opting into future protocol changes." - ) - .argument( - "", - "Set this to true to allow minting, taking terms (limit, cap, height, offset) as restrictions" - ) + .argument("", "Relative block height where minting closes") + .argument("", "Amount of allocated dunes to the etcher while etching") + .argument("", "Marks this etching as opting into future protocol changes") + .argument("", "Set to true to allow minting with terms") + .argument("[parentId]", "Optional parent inscription ID (e.g., e6c6...i0)") // New optional argument .action( async ( tick, @@ -905,24 +895,16 @@ program offsetEnd, premine, turbo, - openMint + openMint, + parentId // New parameter ) => { console.log("Deploying open Dune..."); console.log( - tick, - symbol, - limit, - divisibility, - cap, - heightStart, - heightEnd, - offsetStart, - offsetEnd, - premine, - turbo, - openMint + tick, symbol, limit, divisibility, cap, heightStart, heightEnd, + offsetStart, offsetEnd, premine, turbo, openMint, parentId ); + // Parse arguments, allowing "null" or undefined cap = cap === "null" ? null : cap; heightStart = heightStart === "null" ? null : heightStart; heightEnd = heightEnd === "null" ? null : heightEnd; @@ -930,29 +912,25 @@ program offsetEnd = offsetEnd === "null" ? null : offsetEnd; premine = premine === "null" ? null : premine; turbo = turbo === "null" ? null : turbo === "true"; - openMint = openMint.toLowerCase() === "true"; if (symbol) { if (symbol.length !== 1 && !isSingleEmoji(symbol)) { - console.error( - `Error: The argument symbol should have exactly 1 character, but is '${symbol}'` - ); + console.error(`Error: Symbol must be 1 character, got '${symbol}'`); process.exit(1); } } const spacedDune = spacedDunefromStr(tick); - const blockcount = await getblockcount(); - const mininumAtCurrentHeight = minimumAtHeight(blockcount.data.result); + const minimumAtCurrentHeight = minimumAtHeight(blockcount.data.result); - if (spacedDune.dune.value < mininumAtCurrentHeight) { - const minAtCurrentHeightObj = { _value: mininumAtCurrentHeight }; + if (spacedDune.dune.value < minimumAtCurrentHeight) { + const minAtCurrentHeightObj = { _value: minimumAtCurrentHeight }; format.call(minAtCurrentHeightObj, formatter); console.error("Dune characters are invalid at current height."); process.stdout.write( - `minimum at current height: ${mininumAtCurrentHeight} possible lowest tick: ${formatter.output}\n` + `minimum at current height: ${minimumAtCurrentHeight} possible lowest tick: ${formatter.output}\n` ); console.log(`dune: ${tick} value: ${spacedDune.dune.value}`); process.exit(1); @@ -972,33 +950,67 @@ program symbol.codePointAt() ); - // create script for given dune statements + // Create script for Dune etching const script = constructScript(etching, undefined, null, null); - // getting the wallet balance + // Load wallet let wallet = JSON.parse(fs.readFileSync(WALLET_PATH)); let balance = wallet.utxos.reduce((acc, curr) => acc + curr.satoshis, 0); if (balance == 0) throw new Error("no funds"); - // creating new tx + // Create new transaction let tx = new Transaction(); - // first output carries the protocol message + // Add parent UTXO if provided + if (parentId) { + const parentUtxo = await fetchParentUtxo(parentId); + tx.from(parentUtxo); // Add parent as input + console.log(`Added parent UTXO ${parentId} to transaction`); + } + + // First output carries the protocol message and Dune etching tx.addOutput( new dogecore.Transaction.Output({ script: script, satoshis: 0 }) ); - // Create second output to sender if dunes are directly allocated in etching + // Second output to sender if premine is specified if (premine > 0) tx.to(wallet.address, 100_000); + // Fund the transaction with safe UTXOs (excludes parent if it has dunes) await fund(wallet, tx); + // Verify funds + if (tx.inputAmount < tx.outputAmount + tx.getFee()) { + throw new Error("not enough funds to cover outputs and fee"); + } + + // Broadcast the transaction await broadcast(tx, true); - console.log(tx.hash); + console.log(`Dune deployed with tx hash: ${tx.hash}`); } ); +// Helper function to fetch parent UTXO +async function fetchParentUtxo(parentId) { + const txid = parentId.split('i')[0]; // Extract txid from e.g., "e6c6...i0" + const vout = parseInt(parentId.split('i')[1], 10); // Extract vout + try { + const rawTx = await getrawtx(txid); + const txData = rawTx.data.result; + const output = txData.vout[vout]; + return { + txid: txid, + vout: vout, + script: output.scriptPubKey.hex, + satoshis: Math.round(output.value * 1e8), // Convert DOGE to satoshis + }; + } catch (error) { + console.error(`Error fetching parent UTXO ${parentId}:`, error); + throw error; + } +} + async function parseScriptString(scriptString) { const parts = scriptString.split(" "); @@ -1211,10 +1223,12 @@ const createUnsignedEtchTxFromUtxo = ( return tx; }; -program.action("getBlockCount").action(async () => { - const res = await getblockcount(); - console.log(res.data.result); -}); +program + .command("getBlockCount") + .action(async () => { + const res = await getblockcount(); + console.log(res.data.result); + }); // @warning: this method is not dune aware.. so the dunes on the wallet are in danger of being spend program @@ -1359,6 +1373,7 @@ walletCommand async function main() { program.parse(); } + function walletNew() { if (!fs.existsSync(WALLET_PATH)) { const privateKey = new PrivateKey(); @@ -1549,14 +1564,13 @@ function updateWallet(wallet, tx) { }); } -async function getrawtx(tx) { - console.log("tx"); - console.log(tx.toString()); +async function getrawtx(txid) { + console.log("txId:", txid); // Log the transaction ID const body = { jsonrpc: "1.0", id: 0, method: "getrawtransaction", - params: [tx.toString(), true], + params: [txid, true], }; const options = { From d6a1b23d3baa7196379d6fd061103868a09216cc Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:25:09 -0400 Subject: [PATCH 04/16] Update README.md with Parent Commands in Deploy Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f73a11a..23e0af2 100644 --- a/README.md +++ b/README.md @@ -94,17 +94,19 @@ node dunes.js wallet send
Deploy a dune: ``` -node dunes.js deployOpenDune 'RANDOM•DUNE•NAME' +node dunes.js deployOpenDune 'RANDOM•DUNE•NAME' [parentId] ``` -Example for a dune that can be minted for 100 blocks, with a limit of 100000000, 8 decimals, no mint limit, symbol R (emojis also work) and premining 1R for the deployer during deployment. The `false` means the dune will not opt-in for future protocol changes. The `true` means mints are open. +**Note**: The `[parentId]` parameter is optional and links the new Dune to a parent inscription by specifying its inscription ID (e.g., `e6c6efe91b6084eae2c8a2fd6470d3d0dbfbb342f1b8601904f45be8095058e2i0`). To use this, the parent inscription’s private key must be in your wallet (`.wallet.json`). -The `null` means that the parameters won't be included, e.g. for not setting a max nr of mints limit. If absolute and relative block heights are defined, the dune minting terms will use the highest defined for start as the starting block height parameter and then lowest defined end as the ending block height. +**Example**: ``` -node dunes.js deployOpenDune 'RANDOM•DUNE•NAME' 'R' 100000000 8 null null null null 100 100000000 false true +node dunes.js deployOpenDune 'RANDOM•DUNE•NAME' 'R' 100000000 8 null null null null 100 100000000 false true e6c6efe91b6084eae2c8a2fd6470d3d0dbfbb342f1b8601904f45be8095058e2i0 ``` +This deploys a Dune named `RANDOM•DUNE•NAME` with symbol `R`, a limit of 100000000 per mint, 8 decimals, and premines 1R (100 atomic units) to the deployer. It’s linked to the parent inscription `e6c6...i0` + Mint a dune: ``` @@ -167,6 +169,18 @@ Example: node dunes.js sendDunesNoProtocol DDjbkNTHPZAq3f6pzApDzP712V1xqSE2Ya 10 WHO•LET•THE•DUNES•OUT ``` +### Parent Inscriptions +You can link a new Dune to an existing "parent" inscription (e.g., an image or another Dune) using the optional `[parentId]` parameter in `deployOpenDune`. This creates a hierarchical relationship between digital assets. + +**Requirements**: +- The parent inscription’s private key must be in your wallet (`.wallet.json`), as the script spends its UTXO to establish the link. +- The `parentId` must be a valid inscription ID in the format `txid:iN` (e.g., `e6c6efe91b6084eae2c8a2fd6470d3d0dbfbb342f1b8601904f45be8095058e2i0`), where `txid` is the transaction ID and `N` is the output index. + +**Why Use It?**: +- Organize related Dunes under a single parent (e.g., linking single or multiple Dunes to an image). +- Build structured collections with parent-child relationships. + + ## FAQ ### I'm getting ECONNREFUSED errors when minting From c97e09c53f8f3a11a4381cf0836ed595a849625b Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:27:54 -0400 Subject: [PATCH 05/16] Update DUNES.md with Parent inscription info Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- DUNES.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/DUNES.md b/DUNES.md index d4eca5d..5c2deb1 100644 --- a/DUNES.md +++ b/DUNES.md @@ -113,3 +113,17 @@ Malformed dunestones are termed cenotaphs. Dunes input to a transaction with a cenotaph are burned. Dunes etched in a transaction with a cenotaph are set as unmintable. Mints in a transaction with a cenotaph count towards the mint cap, but the minted dunes are burned. Cenotaphs are an upgrade mechanism, allowing dunestones to be given new semantics that change how dunes are created and transferred, while not misleading unupgraded clients as to the location of those dunes, as unupgraded clients will see those dunes as having been burned. + +### Parent Inscriptions +Dunes can be linked to a "parent" inscription, establishing a hierarchical relationship. This allows Dunes to be associated with other digital assets, such as images or other Dunes. + +**How It Works**: +- During etching, you can specify a `parentId` (the inscription ID of the parent, e.g., `e6c6efe91b6084eae2c8a2fd6470d3d0dbfbb342f1b8601904f45be8095058e2i0`). +- The etching transaction spends the parent inscription’s UTXO, embedding the link in the blockchain. +- The etcher must control the parent inscription (i.e., have its private key). + +**Use Cases**: +- Associate multiple Dunes with a single parent (e.g., an image inscription). +- Create structured collections of digital assets with parent-child relationships. + +**Note**: This feature is optional. Dunes can be etched without a parent if no `parentId` is provided. From ad76f08068fb4d56447efeee4daa90cfaaa60040 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:29:18 -0400 Subject: [PATCH 06/16] Update README.md added info to link a new Dune to an existing "parent" inscription (e.g., an image or another Dune) using the optional `[parentId]` Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 23e0af2..6191f95 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,8 @@ You can link a new Dune to an existing "parent" inscription (e.g., an image or a - Organize related Dunes under a single parent (e.g., linking single or multiple Dunes to an image). - Build structured collections with parent-child relationships. +**Note**: This feature is optional. Dunes can be etched without a parent if no `parentId` is provided. + ## FAQ From a65e43d9016909deb8abce038c7eb40cea8860fa Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:52:25 -0400 Subject: [PATCH 07/16] Update README.md update broken ORD= link Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6191f95..b141480 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ NODE_RPC_USER= NODE_RPC_PASS= TESTNET=false FEE_PER_KB=50000000 -ORD=https://ord.dunesprotocol.com/ +UNSPENT_API=https://unspent.dogeord.io/api/v2/address/unspent/ +ORD=https://wonky-ord-v2.dogeord.io/ ``` You can get the current fee per kb from [here](https://blockchair.com/). From d3d8639ab543d949377b1ff26ce5f7daaa1b8d89 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:14:12 -0400 Subject: [PATCH 08/16] Update README.md removed dead api link Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b141480..ce44ec0 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,6 @@ NODE_RPC_USER= NODE_RPC_PASS= TESTNET=false FEE_PER_KB=50000000 -UNSPENT_API=https://unspent.dogeord.io/api/v2/address/unspent/ ORD=https://wonky-ord-v2.dogeord.io/ ``` From 4dba8b0223c4b215a0069e256f06f4a6585b76f8 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:15:16 -0400 Subject: [PATCH 09/16] Update .env.example deleted dead api link Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index 5a9110e..9947998 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,4 @@ NODE_RPC_USER= NODE_RPC_PASS= TESTNET=false FEE_PER_KB=50000000 -UNSPENT_API=https://unspent.dogeord.io/api/v2/address/unspent/ ORD=https://wonky-ord-v2.dogeord.io/ From 9c55b223cfaf5df06e520f7cdfde065e81f2ef87 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Fri, 21 Mar 2025 18:37:58 -0400 Subject: [PATCH 10/16] Update README.md Important Notes for Build structured collections with parent-child relationships logic Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce44ec0..6c38856 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,12 @@ You can link a new Dune to an existing "parent" inscription (e.g., an image or a - Organize related Dunes under a single parent (e.g., linking single or multiple Dunes to an image). - Build structured collections with parent-child relationships. -**Note**: This feature is optional. Dunes can be etched without a parent if no `parentId` is provided. +**Important Notes**: +- This feature is optional. Dunes can be etched without a parent if no `parentId` is provided. + +- This "Requires" the parent’s private key to be accessible in the wallet. + +- Ensure that the parent inscription’s private key is accessible in the wallet derived from your MetaMask seed phrase. This is necessary because the transaction will spend the parent UTXO to establish the parent-child relationship ## FAQ From 217d7a9ef20659f4b82444eea83b320401fabe13 Mon Sep 17 00:00:00 2001 From: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> Date: Sat, 26 Jul 2025 15:54:10 -0400 Subject: [PATCH 11/16] =?UTF-8?q?Update=20dunes.js=20added=20"apply=20pay?= =?UTF-8?q?=20terms=E2=80=9D=20PR=20from=20Duneys=20Master?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Much pull request Such Approved "apply pay terms” is the Key: it lets you set a mint price (in “shibes”) and specify who gets paid when someone mints a Dune. Previously, we could only open‐mint with limits, caps, and block‑height restrictions, etc.; now we can also monetize it by charging a fee and directing that fee to an address of your choice. You also refactored the internal broadcast() function’s retryAsync call so it passes the helper directly instead of wrapping it in an extra async lambda: res = await retryAsync(async () => await makePostRequest(), 10, 30000); res = await retryAsync(makePostRequest, 10, 30000); This doesn’t change behavior, it does makes the retry logic a bit cleaner! Why im Digging this upgrade??? Monetization: Projects can now sustainably fund themselves by charging a shibe‑denominated mint fee. Flexibility: Since the fee is just another field in the Terms object, it’s fully optional, so if you don’t want a fee, you pass "null" and everything behaves exactly like before. Such Flexibility!!! Backward‑compatible: Existing scripts and deployments that never set a price will continue to work unchanged. Decentralization & Dogecoin ethics: On‑chain enforcement only: All the fee logic happens in your local CLI client and then on‑chain via the same RPC calls and smart‑contract‑style logic Dunes has always used. There’s no off‑chain "Gatekeeper" or "Centralized service" validating payments. User choice: Anyone can deploy an open mint with or without a fee; you’re not forced to pay if you don’t want to. Transparent rules: All parameters (limits, heights, offsets, fees, pay‑to address) are encoded in the same public transaction data we’ve always put on Dogecoin. No new trust assumptions: You still only need to trust the Dogecoin network itself and your own keypair....there’s no additional trusted infrastructure introduced. In short, it’s Such a clean, opt‑in extension that preserves the fully decentralized ethos of Dogecoin. Much Innovation, Such Decentralized, So Dunes Well done Fam! Thanks for the Great Idea @reallyshadydev https://x.com/reallyshadydev Signed-off-by: GreatApe42069 <153969184+GreatApe42069@users.noreply.github.com> --- dunes.js | 1376 ++++++++++++------------------------------------------ 1 file changed, 288 insertions(+), 1088 deletions(-) diff --git a/dunes.js b/dunes.js index ee33fe6..80814d4 100755 --- a/dunes.js +++ b/dunes.js @@ -11,70 +11,63 @@ const { program } = require("commander"); const bb26 = require("base26"); const prompts = require("prompts"); -const axiosRetryOptions = { +dotenv.config(); + +const ordApi = axios.create({ baseURL: process.env.ORD, timeout: 100_000 }); +axiosRetry(axios, { retries: 10, retryDelay: axiosRetry.exponentialDelay, -}; - -axiosRetry(axios, axiosRetryOptions); - -dotenv.config(); +}); +// add this line to enable retries on ordApi: +axiosRetry(ordApi, { + retries: 10, + retryDelay: axiosRetry.exponentialDelay, +}); -if (process.env.TESTNET == "true") { +if (process.env.TESTNET === "true") { dogecore.Networks.defaultNetwork = dogecore.Networks.testnet; } if (process.env.FEE_PER_KB) { - Transaction.FEE_PER_KB = parseInt(process.env.FEE_PER_KB); + Transaction.FEE_PER_KB = parseInt(process.env.FEE_PER_KB, 10); } else { - Transaction.FEE_PER_KB = 100000000; + Transaction.FEE_PER_KB = 100_000_000; } const WALLET_PATH = process.env.WALLET || ".wallet.json"; - const IDENTIFIER = stringToCharCodes(process.env.PROTOCOL_IDENTIFIER); - const MAX_SCRIPT_ELEMENT_SIZE = 520; class PushBytes { constructor(bytes) { this.bytes = Buffer.from(bytes); } - static fromSliceUnchecked(bytes) { return new PushBytes(bytes); } - static fromMutSliceUnchecked(bytes) { return new PushBytes(bytes); } - static empty() { return new PushBytes([]); } - asBytes() { return this.bytes; } - asMutBytes() { return this.bytes; } } -// Encode a u128 value to a byte array function varIntEncode(n) { const out = new Array(19).fill(0); let i = 18; - out[i] = Number(BigInt(n) & 0b01111111n); - while (BigInt(n) > 0b01111111n) { n = BigInt(n) / 128n - 1n; i -= 1; out[i] = Number(BigInt(n) | 0b10000000n); } - return out.slice(i); } @@ -91,9 +84,7 @@ class Tag { static HeightEnd = 18; static Cap = 20; static Premine = 22; - static Cenotaph = 254; - static Divisibility = 1; static Spacers = 3; static Symbol = 5; @@ -102,10 +93,9 @@ class Tag { static take(tag, fields) { return fields[tag]; } - static encode(tag, value, payload) { payload.push(varIntEncode(tag)); - if (tag == Tag.Dune) payload.push(encodeToTuple(value)); + if (tag === Tag.Dune) payload.push(encodeToTuple(value)); else payload.push(varIntEncode(value)); } } @@ -119,20 +109,17 @@ class Flag { static mask(flag) { return BigInt(1) << BigInt(flag); } - static take(flag, flags) { - const mask = Flag.mask(flag); - const set = (flags & mask) !== 0n; - flags &= ~mask; + const m = Flag.mask(flag); + const set = (flags & m) !== 0n; + flags &= ~m; return set; } - static set(flag, flags) { - flags |= Flag.mask(flag); + return flags | Flag.mask(flag); } } -// Construct the OP_RETURN dune script with encoding of given values function constructScript( etching = null, pointer = undefined, @@ -142,7 +129,6 @@ function constructScript( const payload = []; if (etching) { - // Setting flags for etching and minting let flags = Number(Flag.mask(Flag.Etch)); if (etching.turbo) flags |= Number(Flag.mask(Flag.Turbo)); if (etching.terms) flags |= Number(Flag.mask(Flag.Terms)); @@ -150,22 +136,15 @@ function constructScript( if (etching.dune) Tag.encode(Tag.Dune, etching.dune, payload); if (etching.terms) { - if (etching.terms.limit) - Tag.encode(Tag.Limit, etching.terms.limit, payload); + if (etching.terms.limit) Tag.encode(Tag.Limit, etching.terms.limit, payload); if (etching.terms.cap) Tag.encode(Tag.Cap, etching.terms.cap, payload); - if (etching.terms.offsetStart) - Tag.encode(Tag.OffsetStart, etching.terms.offsetStart, payload); - if (etching.terms.offsetEnd) - Tag.encode(Tag.OffsetEnd, etching.terms.offsetEnd, payload); - if (etching.terms.heightStart) - Tag.encode(Tag.HeightStart, etching.terms.heightStart, payload); - if (etching.terms.heightEnd) - Tag.encode(Tag.HeightEnd, etching.terms.heightEnd, payload); + if (etching.terms.offsetStart) Tag.encode(Tag.OffsetStart, etching.terms.offsetStart, payload); + if (etching.terms.offsetEnd) Tag.encode(Tag.OffsetEnd, etching.terms.offsetEnd, payload); + if (etching.terms.heightStart) Tag.encode(Tag.HeightStart, etching.terms.heightStart, payload); + if (etching.terms.heightEnd) Tag.encode(Tag.HeightEnd, etching.terms.heightEnd, payload); } - if (etching.divisibility !== 0) - Tag.encode(Tag.Divisibility, etching.divisibility, payload); - if (etching.spacers !== 0) - Tag.encode(Tag.Spacers, etching.spacers, payload); + if (etching.divisibility !== 0) Tag.encode(Tag.Divisibility, etching.divisibility, payload); + if (etching.spacers !== 0) Tag.encode(Tag.Spacers, etching.spacers, payload); if (etching.symbol) Tag.encode(Tag.Symbol, etching.symbol, payload); if (etching.premine) Tag.encode(Tag.Premine, etching.premine, payload); } @@ -184,32 +163,23 @@ function constructScript( const sortedEdicts = edicts.slice().sort((a, b) => { const idA = BigInt(a.id); const idB = BigInt(b.id); - return idA < idB ? -1 : idA > idB ? 1 : 0; }); - let id = 0; + let lastId = 0n; for (const edict of sortedEdicts) { - if (typeof edict.id === "bigint") - payload.push(varIntEncode(edict.id - BigInt(id))); - else payload.push(varIntEncode(edict.id - id)); + payload.push(varIntEncode(edict.id - lastId)); payload.push(varIntEncode(edict.amount)); payload.push(varIntEncode(edict.output)); - id = edict.id; + lastId = edict.id; } } - // Create script with protocol message - let script = createScriptWithProtocolMsg(); - - // Flatten the nested arrays in the tuple representation - const flattenedTuple = payload.flat(); - - // Push payload bytes to script - for (let i = 0; i < flattenedTuple.length; i += MAX_SCRIPT_ELEMENT_SIZE) { - const chunk = flattenedTuple.slice(i, i + MAX_SCRIPT_ELEMENT_SIZE); - const push = PushBytes.fromSliceUnchecked(chunk); - script.add(Buffer.from(push.asBytes())); + const script = new Script().add("OP_RETURN").add(Buffer.from(IDENTIFIER)); + const flattened = payload.flat(); + for (let i = 0; i < flattened.length; i += MAX_SCRIPT_ELEMENT_SIZE) { + const chunk = flattened.slice(i, i + MAX_SCRIPT_ELEMENT_SIZE); + script.add(Buffer.from(PushBytes.fromSliceUnchecked(chunk).asBytes())); } return script; @@ -229,58 +199,33 @@ class Dune { } function parseDuneFromString(s) { - let x = BigInt(0); - + let x = 0n; for (let i = 0; i < s.length; i++) { - if (i > 0) { - x += BigInt(1); - } - - x *= BigInt(26); - - const charCode = s.charCodeAt(i); - - if (charCode >= "A".charCodeAt(0) && charCode <= "Z".charCodeAt(0)) { - x += BigInt(charCode - "A".charCodeAt(0)); - } else { - throw new Error(`Invalid character in dune name: ${s[i]}`); - } + if (i > 0) x += 1n; + x *= 26n; + const c = s.charCodeAt(i); + if (c >= 65 && c <= 90) x += BigInt(c - 65); + else throw new Error(`Invalid character in dune name: ${s[i]}`); } - return new Dune(x); } -// Function to parse a string into a SpacedDune in Node.js function spacedDunefromStr(s) { let dune = ""; let spacers = 0; - for (const c of s) { - switch (true) { - case /[A-Z]/.test(c): - dune += c; - break; - case /[.•]/.test(c): - const flag = 1 << (dune.length - 1); - if ((spacers & flag) !== 0) { - throw new Error("double spacer"); - } - spacers |= flag; - break; - default: - throw new Error("invalid character"); - } - } - - if (32 - Math.clz32(spacers) >= dune.length) { - throw new Error("trailing spacer"); - } - + if (/[A-Z]/.test(c)) dune += c; + else if (/[\u2022\u2023]/.test(c)) { + const flag = 1 << (dune.length - 1); + if ((spacers & flag) !== 0) throw new Error("double spacer"); + spacers |= flag; + } else throw new Error("invalid character"); + } + if (32 - Math.clz32(spacers) >= dune.length) throw new Error("trailing spacer"); return new SpacedDune(dune, spacers); } class Edict { - // Constructor for Edict constructor(id, amount, output) { this.id = id; this.amount = amount; @@ -289,18 +234,18 @@ class Edict { } class Terms { - constructor(limit, cap, offsetStart, offsetEnd, heightStart, heightEnd) { + constructor(limit, cap, offsetStart, offsetEnd, heightStart, heightEnd, price = null) { this.limit = limit !== undefined ? limit : null; this.cap = cap !== undefined ? cap : null; this.offsetStart = offsetStart !== undefined ? offsetStart : null; this.offsetEnd = offsetEnd !== undefined ? offsetEnd : null; this.heightStart = heightStart !== undefined ? heightStart : null; this.heightEnd = heightEnd !== undefined ? heightEnd : null; + if (price) this.price = price; } } class Etching { - // Constructor for Etching constructor(divisibility, terms, turbo, premine, dune, spacers, symbol) { this.divisibility = divisibility; this.terms = terms !== undefined ? terms : null; @@ -353,77 +298,39 @@ const STEPS = [ const SUBSIDY_HALVING_INTERVAL_10X = 2100000n; const FIRST_DUNE_HEIGHT = 5084000n; - const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -function format(formatter) { - let n = BigInt(this._value); - - if (n === 2n ** 128n - 1n) { - return formatter.write("BCGDENLQRQWDSLRUGSNLBTMFIJAV"); - } - - n += 1n; - let symbol = ""; - - while (n > 0n) { - symbol += ALPHABET.charAt(Number((n - 1n) % 26n)); - n = (n - 1n) / 26n; - } - - for (const c of symbol.split("").reverse()) { - formatter.write(c); - } -} - -const formatter = { - output: "", - write(str) { - this.output += str; - return this; - }, -}; - function minimumAtHeight(height) { const offset = BigInt(height) + 1n; - const INTERVAL = SUBSIDY_HALVING_INTERVAL_10X / 12n; - const start = FIRST_DUNE_HEIGHT; const end = start + SUBSIDY_HALVING_INTERVAL_10X; if (offset < start) { return BigInt(STEPS[12]); } - if (offset >= end) { return 0n; } const progress = offset - start; - - const length = BigInt(12 - Math.floor(Number(progress / INTERVAL))); - - const endValue = BigInt(STEPS[length - 1n]); + const length = 12 - Math.floor(Number(progress / INTERVAL)); const startValue = BigInt(STEPS[length]); + const endValue = BigInt(STEPS[length - 1]); const remainder = progress % INTERVAL; - return startValue - ((startValue - endValue) * remainder) / INTERVAL; } function encodeToTuple(n) { const tupleRepresentation = []; - - tupleRepresentation.push(Number(n & BigInt(0b0111_1111))); - - while (n > BigInt(0b0111_1111)) { - n = n / BigInt(128) - BigInt(1); + tupleRepresentation.push(Number(n & 0b01111111n)); + while (n > 0b01111111n) { + n = n / 128n - 1n; tupleRepresentation.unshift( - Number((n & BigInt(0b0111_1111)) | BigInt(0b1000_0000)) + Number((n & 0b01111111n) | 0b10000000n) ); } - return tupleRepresentation; } @@ -436,13 +343,10 @@ program const getUtxosWithDunes = []; const CHUNK_SIZE = 10; - // Helper function to process a chunk of UTXOs async function processChunk(utxosChunk, startIndex) { const promises = utxosChunk.map((utxo, index) => { console.log( - `Processing utxo number ${startIndex + index} of ${ - wallet.utxos.length - }` + `Processing utxo number ${startIndex + index} of ${wallet.utxos.length}` ); return getDunesForUtxo(`${utxo.txid}:${utxo.vout}`).then( (dunesOnUtxo) => { @@ -460,7 +364,6 @@ program } } - // Process UTXOs in chunks for (let i = 0; i < wallet.utxos.length; i += CHUNK_SIZE) { const chunk = wallet.utxos.slice(i, i + CHUNK_SIZE); await processChunk(chunk, i); @@ -480,22 +383,18 @@ program const utxos = await fetchAllUnspentOutputs(address); let balance = 0n; const utxoHashes = utxos.map((utxo) => `${utxo.txid}:${utxo.vout}`); - const chunkSize = 10; // Size of each chunk + const chunkSize = 10; - // Function to chunk the utxoHashes array const chunkedUtxoHashes = []; for (let i = 0; i < utxoHashes.length; i += chunkSize) { chunkedUtxoHashes.push(utxoHashes.slice(i, i + chunkSize)); } - // Process each chunk for (const chunk of chunkedUtxoHashes) { const allDunes = await getDunesForUtxos(chunk); - for (const dunesInfo of allDunes) { for (const singleDunesInfo of dunesInfo.dunes) { const [name, { amount }] = singleDunesInfo; - if (name === dune_name) { balance += BigInt(amount); } @@ -503,7 +402,6 @@ program } } - // Output the total balance console.log(`${balance.toString()} ${dune_name}`); }); @@ -539,7 +437,6 @@ const getUtxosWithOutDunes = async () => { }; const parseDuneId = (id, claim = false) => { - // Check if Dune ID is in the expected format const regex1 = /^\d+\:\d+$/; const regex2 = /^\d+\/\d+$/; @@ -548,17 +445,13 @@ const parseDuneId = (id, claim = false) => { `Dune ID ${id} is not in the expected format e.g. 1234:1 or 1234/1` ); - // Parse the id string to get height and index const [heightStr, indexStr] = regex1.test(id) ? id.split(":") : id.split("/"); - const height = parseInt(heightStr, 10); - const index = parseInt(indexStr, 10); + const height = BigInt(parseInt(heightStr, 10)); + const index = BigInt(parseInt(indexStr, 10)); - // Set the bits in the id using bitwise OR - let duneId = (BigInt(height) << BigInt(16)) | BigInt(index); - - // For minting set CLAIM_BIT + let duneId = (height << 16n) | index; if (claim) { - const CLAIM_BIT = BigInt(1) << BigInt(48); + const CLAIM_BIT = 1n << 48n; duneId |= CLAIM_BIT; } @@ -566,7 +459,6 @@ const parseDuneId = (id, claim = false) => { }; const createScriptWithProtocolMsg = () => { - // create an OP_RETURN script with the protocol message return new dogecore.Script().add("OP_RETURN").add(Buffer.from(IDENTIFIER)); }; @@ -580,9 +472,9 @@ program .argument("", "Amounts to send, separated by comma") .argument("", "Receiver's addresses, separated by comma") .action(async (txhash, vout, dune, decimals, amounts, addresses) => { - const amountsAsArray = amounts.split(",").map((amount) => Number(amount)); + const amountsAsArray = amounts.split(",").map((a) => Number(a)); const addressesAsArray = addresses.split(","); - if (amountsAsArray.length != addressesAsArray.length) { + if (amountsAsArray.length !== addressesAsArray.length) { console.error( `length of amounts ${amountsAsArray.length} and addresses ${addressesAsArray.length} are different` ); @@ -613,7 +505,7 @@ program try { const res = await walletSendDunesNoProtocol( address, - parseInt(utxoAmount), + parseInt(utxoAmount, 10), dune ); console.info(`Broadcasted transaction: ${JSON.stringify(res)}`); @@ -623,7 +515,6 @@ program } }); -// sends the full balance of the specified dune async function walletSendDunes( txhash, vout, @@ -635,97 +526,48 @@ async function walletSendDunes( let wallet = JSON.parse(fs.readFileSync(WALLET_PATH)); const dune_utxo = wallet.utxos.find( - (utxo) => utxo.txid == txhash && utxo.vout == vout + (u) => u.txid === txhash && u.vout === parseInt(vout, 10) ); - if (!dune_utxo) { - console.error(`utxo ${txhash}:${vout} not found`); - throw new Error(`utxo ${txhash}:${vout} not found`); - } + if (!dune_utxo) throw new Error(`utxo ${txhash}:${vout} not found`); const dunes = await getDunesForUtxo(`${dune_utxo.txid}:${dune_utxo.vout}`); - if (dunes.length == 0) throw new Error("no dunes"); + if (dunes.length === 0) throw new Error("no dunes"); - // check if the dune is in the utxo and if we have enough amount - const duneOnUtxo = dunes.find((d) => d.dune == dune); + const duneOnUtxo = dunes.find((d) => d.dune === dune); + if (!duneOnUtxo) throw new Error("dune not found"); - // Extract the numeric part from duneOnUtxo.amount as a BigInt let duneOnUtxoAmount = BigInt(duneOnUtxo.amount.match(/\d+/)[0]); - - // Add the decimals duneOnUtxoAmount *= BigInt(10 ** decimals); - if (!dune) throw new Error("dune not found"); const totalAmount = amounts.reduce( (acc, curr) => acc + BigInt(curr), - BigInt(0) + 0n ); - console.log("totalAmount", totalAmount); - if (duneOnUtxoAmount < totalAmount) throw new Error("not enough dunes"); - // Define default output where the sender receives unallocated dunes - const DEFAULT_OUTPUT = 1; - // Define output offset for receivers of dunes - const OFFSET = 2; + if (duneOnUtxoAmount < totalAmount) throw new Error("not enough dunes"); - // ask the user to confirm in the cli const response = await prompts({ type: "confirm", name: "value", - message: `Transferring ${totalAmount} of ${dune}. Are you sure you want to proceed?`, + message: `Transferring ${totalAmount} of ${dune}. Proceed?`, initial: true, }); + if (!response.value) throw new Error("Transaction aborted"); - if (!response.value) { - throw new Error("Transaction aborted"); - } - - let tx = new Transaction(); - - tx.from(dune_utxo); - - // we get the dune - const { id, divisibility, limit } = await getDune(dune); - console.log("id", id); - - // parse given id string to dune id - const duneId = parseDuneId(id); - - /** - * we have an index-offset of 2 - * - the first output (index 0) is the protocol message - * - the second output (index 1) is where we put the dunes which are on input utxos which shouldn't be transfered - * */ - const edicts = []; - for (let i = 0; i < amounts.length; i++) { - edicts.push(new Edict(duneId, amounts[i], i + OFFSET)); - } + const DEFAULT_OUTPUT = 1; + const OFFSET = 2; + const edicts = amounts.map((amt, i) => new Edict(parseDuneId(dune), amt, i + OFFSET)); - // Create payload and parse it into an OP_RETURN script with protocol message const script = constructScript(null, DEFAULT_OUTPUT, null, edicts); - - // Add output with OP_RETURN Dune assignment script - tx.addOutput( - new dogecore.Transaction.Output({ script: script, satoshis: 0 }) - ); - - // add one output to the sender for the dunes that are not transferred + const tx = new Transaction(); + tx.addOutput(new Transaction.Output({ script, satoshis: 0 })); tx.to(wallet.address, 100_000); + for (const addr of addresses) tx.to(addr, 100_000); - // the output after the protocol message will carry the dune balance if no payload is specified - for (const address of addresses) { - tx.to(address, 100_000); - } - - // we fund the tx await fund(wallet, tx); + if (tx.inputAmount < tx.outputAmount + tx.getFee()) throw new Error("not enough funds"); - if (tx.inputAmount < tx.outputAmount + tx.getFee()) { - throw new Error("not enough funds"); - } - - console.log(tx.toObject()); await broadcast(tx, true); - console.log(tx.hash); } @@ -737,151 +579,94 @@ async function walletSendDunesNoProtocol(address, utxoAmount, dune) { ); const duneOutputMap = new Map(); - for (const dune of walletBalanceFromOrd.data.dunes) { - for (const balance of dune.balances) { - duneOutputMap.set(balance.txid, { - ...balance, - dune: dune.dune, - }); + for (const d of walletBalanceFromOrd.data.dunes) { + for (const b of d.balances) { + duneOutputMap.set(b.txid, { ...b, dune: d.dune }); } } - const nonDuneUtxos = wallet.utxos.filter( - (utxo) => !duneOutputMap.has(utxo.txid) - ); + const nonDuneUtxos = wallet.utxos.filter((u) => !duneOutputMap.has(u.txid)); + if (nonDuneUtxos.length === 0) throw new Error("no utxos without dunes found"); - if (nonDuneUtxos.length === 0) { - throw new Error("no utxos without dunes found"); - } - - const gasUtxo = nonDuneUtxos.find((utxo) => utxo.satoshis > 100_000_000); - - if (!gasUtxo) { - throw new Error(`no gas utxo found`); - } - - let dunesUtxosValue = 0; const dunesUtxos = []; - - for (const utxo of wallet.utxos) { - if (dunesUtxos.length >= utxoAmount) { - break; - } - - if (duneOutputMap.has(utxo.txid)) { - const duneOutput = duneOutputMap.get(utxo.txid); - if (duneOutput.dune === dune) { - dunesUtxos.push(utxo); - dunesUtxosValue += utxo.satoshis; - } + for (const u of wallet.utxos) { + if (dunesUtxos.length >= utxoAmount) break; + if (duneOutputMap.has(u.txid) && duneOutputMap.get(u.txid).dune === dune) { + dunesUtxos.push(u); } } + if (dunesUtxos.length < utxoAmount) throw new Error("not enough dune utxos found"); - if (dunesUtxos.length < utxoAmount) { - throw new Error(`not enough dune utxos found`); - } - - const response = await prompts({ + const resp = await prompts({ type: "confirm", name: "value", - message: `Transferring ${utxoAmount} utxos of ${dune}. Are you sure you want to proceed?`, + message: `Transferring ${utxoAmount} utxos of ${dune}. Proceed?`, initial: true, }); + if (!resp.value) throw new Error("Transaction aborted"); - if (!response.value) { - throw new Error("Transaction aborted"); - } - - let tx = new Transaction(); + const tx = new Transaction(); tx.from(dunesUtxos); - tx.to(address, dunesUtxosValue); - + tx.to(address, dunesUtxos.reduce((acc, u) => acc + u.satoshis, 0)); await fund(wallet, tx); return await broadcast(tx, true); } const _mintDune = async (id, amount, receiver) => { - console.log("Minting Dune..."); - console.log(id, amount, receiver); - - // Parse given id string to dune id + console.log("Minting Dune...", id, amount, receiver); const duneId = parseDuneId(id, true); - if (amount == 0) { - const { id_, divisibility, limit } = await getDune(id); + if (amount === 0) { + const { divisibility, limit } = await getDune(id); amount = BigInt(limit) * BigInt(10 ** divisibility); } - // mint dune with encoded id, amount on output 1 const edicts = [new Edict(duneId, amount, 1)]; - console.log(edicts); - - // Create script for given dune statements const script = constructScript(null, undefined, null, edicts); - - // getting the wallet balance let wallet = JSON.parse(fs.readFileSync(WALLET_PATH)); - let balance = wallet.utxos.reduce((acc, curr) => acc + curr.satoshis, 0); - if (balance == 0) throw new Error("no funds"); - - // creating new tx - let tx = new Transaction(); + if (wallet.utxos.length === 0) throw new Error("no funds"); - // output carries the protocol message - tx.addOutput( - new dogecore.Transaction.Output({ script: script, satoshis: 0 }) - ); - - // add receiver output holding dune amount + const tx = new Transaction(); + tx.addOutput(new Transaction.Output({ script, satoshis: 0 })); tx.to(receiver, 100_000); - await fund(wallet, tx); - try { - await broadcast(tx, true); - } catch (e) { - console.log(e); - } - + await broadcast(tx, true).catch(console.log); console.log(tx.hash); }; program .command("mintDune") .description("Mint a Dune") - .argument("", "id of the dune in format block:index e.g. 5927764:2") - .argument( - "", - "amount to mint (0 takes the limit of the dune as amount)" - ) - .argument("", "address of the receiver") + .argument("", "block:index e.g. 5927764:2") + .argument("", "amount to mint (0 uses limit)") + .argument("", "receiver address") .action(_mintDune); function isSingleEmoji(str) { const emojiRegex = /[\p{Emoji}]/gu; - const matches = str.match(emojiRegex); - return matches ? matches.length === 1 : false; } -// Updated deployOpenDune command with optional parentId program .command("deployOpenDune") - .description("Deploy a Dune that is open for mint with an optional parent inscription") + .description("Deploy a Dune that is open for mint with optional parent inscription") .argument("", "Tick for the dune") - .argument("", "symbol") - .argument("", "Max amount that can be minted in one transaction") - .argument("", "divisibility of the dune. Max 38") - .argument("", "Max limit that can be minted overall") - .argument("", "Absolute block height where minting opens") - .argument("", "Absolute block height where minting closes") - .argument("", "Relative block height where minting opens") - .argument("", "Relative block height where minting closes") - .argument("", "Amount of allocated dunes to the etcher while etching") - .argument("", "Marks this etching as opting into future protocol changes") - .argument("", "Set to true to allow minting with terms") - .argument("[parentId]", "Optional parent inscription ID (e.g., e6c6...i0)") // New optional argument + .argument("", "Symbol") + .argument("", "Max mint per tx") + .argument("", "Divisibility") + .argument("", "Overall mint cap or 'null'") + .argument("", "Open height or 'null'") + .argument("", "Close height or 'null'") + .argument("", "Open offset or 'null'") + .argument("", "Close offset or 'null'") + .argument("", "Premine amount or 'null'") + .argument("", "Turbo flag true/false") + .argument("", "Enable minting true/false") + .argument("[parentId]", "Optional parent inscription ID") + .argument("", "Mint price in shibes or 'null'") + .argument("", "Pay-to address or 'null'") .action( async ( tick, @@ -896,48 +681,40 @@ program premine, turbo, openMint, - parentId // New parameter + parentId, + priceAmount, + pricePayTo ) => { - console.log("Deploying open Dune..."); - console.log( - tick, symbol, limit, divisibility, cap, heightStart, heightEnd, - offsetStart, offsetEnd, premine, turbo, openMint, parentId - ); - - // Parse arguments, allowing "null" or undefined + console.log("Deploying open Dune with pay terms…"); cap = cap === "null" ? null : cap; heightStart = heightStart === "null" ? null : heightStart; heightEnd = heightEnd === "null" ? null : heightEnd; offsetStart = offsetStart === "null" ? null : offsetStart; offsetEnd = offsetEnd === "null" ? null : offsetEnd; premine = premine === "null" ? null : premine; - turbo = turbo === "null" ? null : turbo === "true"; + turbo = turbo === "true"; openMint = openMint.toLowerCase() === "true"; - if (symbol) { - if (symbol.length !== 1 && !isSingleEmoji(symbol)) { - console.error(`Error: Symbol must be 1 character, got '${symbol}'`); - process.exit(1); - } + if (symbol && symbol.length !== 1 && !isSingleEmoji(symbol)) { + console.error(`Error: Symbol must be 1 character, got '${symbol}'`); + process.exit(1); } const spacedDune = spacedDunefromStr(tick); - const blockcount = await getblockcount(); - const minimumAtCurrentHeight = minimumAtHeight(blockcount.data.result); - - if (spacedDune.dune.value < minimumAtCurrentHeight) { - const minAtCurrentHeightObj = { _value: minimumAtCurrentHeight }; - format.call(minAtCurrentHeightObj, formatter); + const { data: blockRes } = await getblockcount(); + const minAtCurrent = minimumAtHeight(blockRes.result); + if (spacedDune.dune.value < minAtCurrent) { console.error("Dune characters are invalid at current height."); - process.stdout.write( - `minimum at current height: ${minimumAtCurrentHeight} possible lowest tick: ${formatter.output}\n` - ); - console.log(`dune: ${tick} value: ${spacedDune.dune.value}`); process.exit(1); } + let price = null; + if (priceAmount !== "null" && pricePayTo !== "null") { + price = { amount: priceAmount, pay_to: pricePayTo }; + } + const terms = openMint - ? new Terms(limit, cap, offsetStart, offsetEnd, heightStart, heightEnd) + ? new Terms(limit, cap, offsetStart, offsetEnd, heightStart, heightEnd, price) : null; const etching = new Etching( @@ -947,281 +724,179 @@ program premine, spacedDune.dune.value, spacedDune.spacers, - symbol.codePointAt() + symbol.codePointAt(0) ); - // Create script for Dune etching const script = constructScript(etching, undefined, null, null); - // Load wallet - let wallet = JSON.parse(fs.readFileSync(WALLET_PATH)); - let balance = wallet.utxos.reduce((acc, curr) => acc + curr.satoshis, 0); - if (balance == 0) throw new Error("no funds"); + const wallet = JSON.parse(fs.readFileSync(WALLET_PATH)); + let balance = wallet.utxos.reduce((a, c) => a + c.satoshis, 0); + if (balance === 0) throw new Error("no funds"); - // Create new transaction let tx = new Transaction(); - // Add parent UTXO if provided if (parentId) { const parentUtxo = await fetchParentUtxo(parentId); - tx.from(parentUtxo); // Add parent as input + tx.from(parentUtxo); console.log(`Added parent UTXO ${parentId} to transaction`); } - // First output carries the protocol message and Dune etching - tx.addOutput( - new dogecore.Transaction.Output({ script: script, satoshis: 0 }) - ); - - // Second output to sender if premine is specified + tx.addOutput(new Transaction.Output({ script, satoshis: 0 })); if (premine > 0) tx.to(wallet.address, 100_000); - // Fund the transaction with safe UTXOs (excludes parent if it has dunes) await fund(wallet, tx); - // Verify funds if (tx.inputAmount < tx.outputAmount + tx.getFee()) { throw new Error("not enough funds to cover outputs and fee"); } - // Broadcast the transaction await broadcast(tx, true); - console.log(`Dune deployed with tx hash: ${tx.hash}`); } ); -// Helper function to fetch parent UTXO +// Helpers: fetchParentUtxo, getrawtx, getblockcount, broadcast, fund, updateWallet, fetchAllUnspentOutputs, getDunesForUtxos, getDunesForUtxo, getDune, retryAsync, wallet commands… + async function fetchParentUtxo(parentId) { - const txid = parentId.split('i')[0]; // Extract txid from e.g., "e6c6...i0" - const vout = parseInt(parentId.split('i')[1], 10); // Extract vout - try { - const rawTx = await getrawtx(txid); - const txData = rawTx.data.result; - const output = txData.vout[vout]; - return { - txid: txid, - vout: vout, - script: output.scriptPubKey.hex, - satoshis: Math.round(output.value * 1e8), // Convert DOGE to satoshis - }; - } catch (error) { - console.error(`Error fetching parent UTXO ${parentId}:`, error); - throw error; - } + const [txid, i] = parentId.split("i"); + const vout = parseInt(i, 10); + const raw = await getrawtx(txid); + const output = raw.data.result.vout[vout]; + return { + txid, + vout, + script: output.scriptPubKey.hex, + satoshis: Math.round(output.value * 1e8), + }; } -async function parseScriptString(scriptString) { - const parts = scriptString.split(" "); - - // Check if there is an OP_RETURN contained in the script string - if (parts.indexOf("OP_RETURN") === -1) { - throw new Error("No OP_RETURN output"); - } - - // Find the indices of the 'OP_PUSHBYTES' instructions - const pushBytesIndices = parts.reduce((indices, part, index) => { - if (part.startsWith("OP_PUSHBYTES")) { - indices.push(index + 1); - } - return indices; - }, []); - - // If 'OP_PUSHBYTES' not found, assume we got 'OP_RETURN identifier msg' format - if (pushBytesIndices.length < 2) { - pushBytesIndices.push(1); - pushBytesIndices.push(2); - } - - // Extract identifier and message - const identifier = parts[pushBytesIndices[0]]; - - /** - * Check Protocol Identifier - * Ord and most other explorers show this in hex representation. - * Some explorers show this in decimal representation, - * therefore we check both. - **/ - if (identifier != IDENTIFIER) { - if (parseInt(identifier, 16) != IDENTIFIER) { - throw new Error("Couldn't find correct Protocol Identifier."); - } - } - - const msg = parts[pushBytesIndices[1]]; - - // Parse msg to payload bytes - const payload = []; - for (let i = 0; i < msg.length; i += 2) { - payload.push(parseInt(msg.substr(i, 2), 16)); - } - - return payload; +async function getrawtx(txid) { + const body = { jsonrpc: "1.0", id: 0, method: "getrawtransaction", params: [txid, true] }; + const opts = { auth: { username: process.env.NODE_RPC_USER, password: process.env.NODE_RPC_PASS } }; + return await axios.post(process.env.NODE_RPC_URL, body, opts); } -async function decodePayload(payload) { - const integers = []; - let i = 0; - - while (i < payload.length) { - const [integer, length] = varIntDecode(payload.slice(i)); - integers.push(integer); - i += length; - } - - return integers; +async function getblockcount() { + const body = { jsonrpc: "1.0", id: 0, method: "getblockcount", params: [] }; + const opts = { auth: { username: process.env.NODE_RPC_USER, password: process.env.NODE_RPC_PASS } }; + return await axios.post(process.env.NODE_RPC_URL, body, opts); } -function varIntDecode(buffer) { - let n = 0n; - let i = 0; - +async function broadcast(tx, retry) { + const body = { jsonrpc: "1.0", id: 0, method: "sendrawtransaction", params: [tx.toString()] }; + const opts = { auth: { username: process.env.NODE_RPC_USER, password: process.env.NODE_RPC_PASS } }; + const makePostRequest = async () => { + try { + return await axios.post(process.env.NODE_RPC_URL, body, opts); + } catch { + return await axios.post(process.env.FALLBACK_NODE_RPC_URL || process.env.NODE_RPC_URL, body, opts); + } + }; + let res; while (true) { - if (i < buffer.length) { - const b = BigInt(parseInt(buffer[i], 10)); - n = n * 128n; - - if (b < 128) { - return [n + b, i + 1]; + try { + res = await retryAsync(makePostRequest, 10, 30000); + break; + } catch (e) { + if (!retry) throw e; + const msg = e.response?.data?.error?.message; + if (msg?.includes("too-long-mempool-chain")) { + console.warn("retrying in 15 secs, too-long-mempool-chain"); + const blockRes = await getblockcount(); + console.log(`Block is ${blockRes.data.result}`); + await new Promise((r) => setTimeout(r, 15000)); + } else { + await walletSync(); + console.log(`Made a wallet sync for address ${JSON.parse(fs.readFileSync(WALLET_PATH)).address}`); + throw e; } - - n = n + b - 127n; - - i++; - } else { - return [n, i]; } } + const wallet = JSON.parse(fs.readFileSync(WALLET_PATH)); + updateWallet(wallet, tx); + fs.writeFileSync(WALLET_PATH, JSON.stringify(wallet, null, 2)); + return res.data; } -function parseIntegers(integers) { - const edicts = []; - const fields = {}; - - for (let i = 0; i < integers.length; i += 2) { - const tag = integers[i]; - if (tag === BigInt(Tag.Body)) { - let id = 0n; - for (let j = i + 1; j < integers.length; j += 3) { - const chunk = integers.slice(j, j + 3); - id = id + BigInt(parseInt(chunk[0], 10)); - edicts.push({ - id, - amount: chunk[1], - output: chunk[2], - }); - } - break; - } - - if (i + 1 <= integers.length) { - const value = integers[i + 1]; - if (!fields[tag]) fields[tag] = value; - } else { - break; - } +async function fund(wallet, tx, onlySafeUtxos = true) { + const utxos = onlySafeUtxos ? await getUtxosWithOutDunes() : wallet.utxos; + const sorted = utxos.slice().sort((a, b) => b.satoshis - a.satoshis); + const large = sorted.filter((u) => u.satoshis >= 1_000_000); + const needed = tx.outputs.reduce((a, o) => a + o.satoshis, 0); + let added = 0n, haveChange = false; + for (const u of large) { + if (added >= needed + BigInt(tx._estimateFee())) break; + tx.from(u); + delete tx._fee; + tx.change(wallet.address); + haveChange = true; + added += BigInt(u.satoshis); } - - return { fields, edicts }; + tx._fee = tx._estimateFee(); + tx.sign(wallet.privkey); + if (!haveChange) throw new Error("no change output added"); + if (tx.inputAmount < tx.outputAmount + tx.getFee()) throw new Error("not enough (secure) funds"); } -function writeDuneWithSpacers(dune, spacers) { - let output = ""; - - for (let i = 0n; i < dune.length; i++) { - const c = dune[i]; - output += c; - - if (spacers && i < dune.length - 1 && spacers & (1n << i)) { - output += "•"; +function updateWallet(wallet, tx) { + wallet.utxos = wallet.utxos.filter((u) => + !tx.inputs.some((i) => i.prevTxId.toString("hex") === u.txid && i.outputIndex === u.vout) + ); + tx.outputs.forEach((out, idx) => { + if (out.script.toAddress().toString() === wallet.address) { + wallet.utxos.push({ txid: tx.hash, vout: idx, script: out.script.toHex(), satoshis: out.satoshis }); } - } + }); +} - return output; +async function fetchAllUnspentOutputs(walletAddress) { + const response = await ordApi.get(`utxos/balance/${walletAddress}?show_all=true&show_unsafe=true`); + return (response.data.utxos || []).map((o) => ({ txid: o.txid, vout: o.vout, script: o.script, satoshis: Number(o.shibes) })); } -// todo: this needs an update for the protocol changes -program - .command("decodeDunesScript") - .description("Decode an OP_RETURN Dunes Script") - .argument("