mirror of
https://github.com/Permissionless-Software-Foundation/psffpp-payments.git
synced 2026-09-21 16:52:04 -07:00
Adding consolidate script
This commit is contained in:
@@ -71,13 +71,23 @@ Contracts are immutable. Order matters:
|
|||||||
5. Publish **Pool** P2SH32 address as the deposit address.
|
5. Publish **Pool** P2SH32 address as the deposit address.
|
||||||
6. **Smoke checklist** (small intentional funding only):
|
6. **Smoke checklist** (small intentional funding only):
|
||||||
- Deposit a small amount to the Pool
|
- Deposit a small amount to the Pool
|
||||||
- Run `consolidate` if multiple UTXOs
|
- Run `npm run consolidate -- --dry-run` then `npm run consolidate` if multiple UTXOs (fee taken from pool; needs ≥2 UTXOs and output ≥ `minConsolidation`)
|
||||||
- Confirm `split` fails before the gate
|
- Confirm `split` fails before the gate
|
||||||
- After height: `split` with a fee-paying companion input
|
- After height: `split` with a fee-paying companion input
|
||||||
- `claim` a share with 2-of-3 ECDSA signatures
|
- `claim` a share with 2-of-3 ECDSA signatures
|
||||||
- Optionally test `migrate` to a next-epoch Pool
|
- Optionally test `migrate` to a next-epoch Pool
|
||||||
7. Only then accept production deposits
|
7. Only then accept production deposits
|
||||||
|
|
||||||
|
### Consolidate
|
||||||
|
|
||||||
|
Permissionless merge of PoolContract UTXOs (via [minimal-slp-wallet](https://www.npmjs.com/package/minimal-slp-wallet) for UTXOs/broadcast + CashScript unlock):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run consolidate -- --dry-run
|
||||||
|
npm run consolidate
|
||||||
|
# optional: --fee-rate 1.2 --max-utxos 50 --rest-url https://free-bch.fullstack.cash
|
||||||
|
```
|
||||||
|
|
||||||
### Signing (ECDSA)
|
### Signing (ECDSA)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -224,5 +224,5 @@
|
|||||||
"enforceLocktimeGuard": true
|
"enforceLocktimeGuard": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updatedAt": "2026-08-09T16:19:40.583Z"
|
"updatedAt": "2026-08-09T23:37:20.963Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,5 +86,5 @@
|
|||||||
"enforceLocktimeGuard": true
|
"enforceLocktimeGuard": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updatedAt": "2026-08-09T16:19:40.446Z"
|
"updatedAt": "2026-08-09T23:37:20.852Z"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6020
-19
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -8,6 +8,7 @@
|
|||||||
"compile": "node scripts/compile.mjs",
|
"compile": "node scripts/compile.mjs",
|
||||||
"addresses": "node scripts/addresses.mjs",
|
"addresses": "node scripts/addresses.mjs",
|
||||||
"deploy": "node scripts/deploy.mjs",
|
"deploy": "node scripts/deploy.mjs",
|
||||||
|
"consolidate": "node scripts/consolidate.mjs",
|
||||||
"test": "npm run compile && mocha --timeout 30000 'test/**/*.test.js'",
|
"test": "npm run compile && mocha --timeout 30000 'test/**/*.test.js'",
|
||||||
"test:unit": "npm test"
|
"test:unit": "npm test"
|
||||||
},
|
},
|
||||||
@@ -22,7 +23,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.0.0",
|
"@bitauth/libauth": "^3.0.0",
|
||||||
"cashc": "^0.13.2",
|
"cashc": "^0.13.2",
|
||||||
"cashscript": "^0.13.2"
|
"cashscript": "^0.13.2",
|
||||||
|
"minimal-slp-wallet": "^7.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Merge PoolContract UTXOs into one self-replicating pool output.
|
||||||
|
* Permissionless — no operator signatures. Miner fee is deducted from the pool.
|
||||||
|
*
|
||||||
|
* - CashScript ElectrumNetworkProvider: pool UTXO lookup (P2SH32-safe) + unlock scripts
|
||||||
|
* - minimal-slp-wallet: broadcast via consumer-api (embedded bch-js at wallet.bchjs)
|
||||||
|
*
|
||||||
|
* Note: wallet.getUtxos() rejects P2SH32 cashaddrs on consumer-api, so pool UTXOs
|
||||||
|
* are fetched via CashScript, not the wallet REST adapter.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/consolidate.mjs [config/deploy.mainnet.json] [--dry-run] [--fee-rate 1.2] [--max-utxos 50]
|
||||||
|
*
|
||||||
|
* Docs: https://www.npmjs.com/package/minimal-slp-wallet
|
||||||
|
* https://bch-js.fullstackcash.net
|
||||||
|
*/
|
||||||
|
import BchWallet from 'minimal-slp-wallet'
|
||||||
|
import { TransactionBuilder } from 'cashscript'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import {
|
||||||
|
loadDeployConfig,
|
||||||
|
createProvider,
|
||||||
|
instantiateContracts,
|
||||||
|
root
|
||||||
|
} from './lib.mjs'
|
||||||
|
|
||||||
|
const DEFAULT_REST = 'https://free-bch.fullstack.cash'
|
||||||
|
const DEFAULT_FEE_RATE = 1.2
|
||||||
|
const DEFAULT_MAX_UTXOS = 50
|
||||||
|
|
||||||
|
function parseArgs (argv) {
|
||||||
|
const args = {
|
||||||
|
configPath: join(root, 'config', 'deploy.mainnet.json'),
|
||||||
|
dryRun: false,
|
||||||
|
feeRate: DEFAULT_FEE_RATE,
|
||||||
|
maxUtxos: DEFAULT_MAX_UTXOS,
|
||||||
|
restURL: DEFAULT_REST
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionals = []
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '--dry-run') {
|
||||||
|
args.dryRun = true
|
||||||
|
} else if (a === '--fee-rate') {
|
||||||
|
args.feeRate = Number(argv[++i])
|
||||||
|
} else if (a === '--max-utxos') {
|
||||||
|
args.maxUtxos = Number(argv[++i])
|
||||||
|
} else if (a === '--rest-url') {
|
||||||
|
args.restURL = argv[++i]
|
||||||
|
} else if (a.startsWith('-')) {
|
||||||
|
throw new Error(`Unknown flag: ${a}`)
|
||||||
|
} else {
|
||||||
|
positionals.push(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionals[0]) args.configPath = positionals[0]
|
||||||
|
if (!Number.isFinite(args.feeRate) || args.feeRate <= 0) {
|
||||||
|
throw new Error('--fee-rate must be a positive number')
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(args.maxUtxos) || args.maxUtxos < 2) {
|
||||||
|
throw new Error('--max-utxos must be an integer >= 2')
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createInfraWallet (restURL) {
|
||||||
|
// Ephemeral wallet — consolidate is permissionless; keys are unused.
|
||||||
|
// Used for broadcast (and wallet.bchjs when lower-level helpers are needed).
|
||||||
|
const wallet = new BchWallet(undefined, {
|
||||||
|
interface: 'consumer-api',
|
||||||
|
restURL
|
||||||
|
})
|
||||||
|
await wallet.walletInfoPromise
|
||||||
|
return wallet
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const cfg = loadDeployConfig(args.configPath)
|
||||||
|
const provider = createProvider(cfg.network || 'mainnet')
|
||||||
|
const { pool } = instantiateContracts(cfg, provider)
|
||||||
|
|
||||||
|
const wallet = await createInfraWallet(args.restURL)
|
||||||
|
|
||||||
|
// P2SH32: use CashScript Electrum (consumer-api getUtxos rejects p-prefix cashaddrs).
|
||||||
|
let utxos = await pool.getUtxos()
|
||||||
|
// Pure BCH only (drop any unexpected token UTXOs).
|
||||||
|
utxos = utxos.filter((u) => !u.token)
|
||||||
|
|
||||||
|
if (utxos.length < 2) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: 'Need at least 2 pool UTXOs to consolidate',
|
||||||
|
poolAddress: pool.address,
|
||||||
|
utxoCount: utxos.length,
|
||||||
|
utxos: utxos.map((u) => ({
|
||||||
|
txid: u.txid,
|
||||||
|
vout: u.vout,
|
||||||
|
satoshis: u.satoshis.toString()
|
||||||
|
}))
|
||||||
|
}, null, 2))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utxos.length > args.maxUtxos) {
|
||||||
|
console.warn(`Truncating from ${utxos.length} to ${args.maxUtxos} UTXOs (--max-utxos)`)
|
||||||
|
utxos = utxos.slice(0, args.maxUtxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inSum = utxos.reduce((s, u) => s + u.satoshis, 0n)
|
||||||
|
const minConsolidation = BigInt(cfg.minConsolidation)
|
||||||
|
|
||||||
|
const builder = new TransactionBuilder({ provider })
|
||||||
|
for (const u of utxos) {
|
||||||
|
builder.addInput(u, pool.unlock.consolidate())
|
||||||
|
}
|
||||||
|
// Single self-replicating pool output; fee = inSum - out (deducted from pool).
|
||||||
|
builder.addBchChangeOutputIfNeeded({ to: pool.address, feeRate: args.feeRate })
|
||||||
|
|
||||||
|
if (builder.outputs.length !== 1) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: `Expected exactly 1 pool output, got ${builder.outputs.length}`,
|
||||||
|
hint: 'Surplus may be below dust after fee — add more BCH to the pool'
|
||||||
|
}, null, 2))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const outAmount = builder.outputs[0].amount
|
||||||
|
if (outAmount < minConsolidation) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: `Consolidated output ${outAmount} would be below minConsolidation ${minConsolidation}`,
|
||||||
|
inSum: inSum.toString(),
|
||||||
|
hint: 'Add more BCH to the pool, or lower --fee-rate'
|
||||||
|
}, null, 2))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fee = inSum - outAmount
|
||||||
|
const hex = builder.build()
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
poolAddress: pool.address,
|
||||||
|
inputCount: utxos.length,
|
||||||
|
inSum: inSum.toString(),
|
||||||
|
fee: fee.toString(),
|
||||||
|
feeRate: args.feeRate,
|
||||||
|
outAmount: outAmount.toString(),
|
||||||
|
minConsolidation: minConsolidation.toString(),
|
||||||
|
dryRun: args.dryRun,
|
||||||
|
inputs: utxos.map((u) => ({ txid: u.txid, vout: u.vout, satoshis: u.satoshis.toString() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.dryRun) {
|
||||||
|
console.log(JSON.stringify({ ...summary, hex }, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const txid = await wallet.broadcast({ hex })
|
||||||
|
console.log(JSON.stringify({ ...summary, txid }, null, 2))
|
||||||
|
console.log(`https://bch.loping.net/tx/${txid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user