first commit

This commit is contained in:
Chris Troutner
2026-08-09 09:23:07 -07:00
commit f2dc7d5c2e
18 changed files with 3037 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
/**
* Print deterministic P2SH32 addresses from a deploy config (read-only).
*
* Usage:
* node scripts/addresses.mjs [config/deploy.mainnet.json]
*/
import {
loadDeployConfig,
createProvider,
instantiateContracts,
buildDeploymentRecord,
root
} from './lib.mjs'
import { join } from 'node:path'
const configPath = process.argv[2] || join(root, 'config', 'deploy.mainnet.json')
const cfg = loadDeployConfig(configPath)
const provider = createProvider(cfg.network || 'mainnet')
const inst = instantiateContracts(cfg, provider)
const record = buildDeploymentRecord(cfg, inst)
console.log(JSON.stringify({
network: record.network,
poolAddress: record.poolAddress,
shares: record.nodes.map((n) => ({
name: n.name,
shareAddress: n.shareAddress,
pkh: n.pkh
})),
splitBlockheight: record.splitBlockheight,
minConsolidation: record.minConsolidation,
artifacts: record.artifacts
}, null, 2))
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { compileFile } from 'cashc'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const contractsDir = join(root, 'contracts')
const artifactsDir = join(root, 'artifacts')
mkdirSync(artifactsDir, { recursive: true })
const files = ['ShareContract.cash', 'PoolContract.cash']
for (const file of files) {
const src = join(contractsDir, file)
const artifact = compileFile(src)
const outName = file.replace(/\.cash$/, '.json')
const outPath = join(artifactsDir, outName)
writeFileSync(outPath, JSON.stringify(artifact, null, 2) + '\n')
console.log(`compiled ${file} -> artifacts/${outName}`)
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* Instantiate contracts against the configured network and write a deployment record.
* Does not broadcast or move funds — funding is a normal send to poolAddress.
*
* Usage:
* node scripts/deploy.mjs [config/deploy.mainnet.json]
*/
import { mkdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
loadDeployConfig,
createProvider,
instantiateContracts,
buildDeploymentRecord,
root
} from './lib.mjs'
const configPath = process.argv[2] || join(root, 'config', 'deploy.mainnet.json')
const cfg = loadDeployConfig(configPath)
if ((cfg.network || 'mainnet') === 'mainnet') {
const placeholder = /^0{40}$|^1{40}$|^2{40}$|^3{40}$/i
if (placeholder.test(cfg.treasuryPkh) || cfg.nodes.some((n) => placeholder.test(n.pkh) || /^(02a+|02b+|02c+)/i.test(n.pubkey))) {
console.warn('WARNING: config still looks like example placeholders. Replace with real keys before funding.')
}
}
const provider = createProvider(cfg.network || 'mainnet')
const inst = instantiateContracts(cfg, provider)
const record = buildDeploymentRecord(cfg, inst)
const outDir = join(root, 'deployments')
mkdirSync(outDir, { recursive: true })
const stamp = record.createdAt.replace(/[:.]/g, '-')
const outPath = join(outDir, `${stamp}.json`)
writeFileSync(outPath, JSON.stringify(record, null, 2) + '\n')
console.log(JSON.stringify({
wrote: outPath,
poolAddress: record.poolAddress,
shareAddresses: record.nodes.map((n) => n.shareAddress),
splitBlockheight: record.splitBlockheight,
artifacts: record.artifacts
}, null, 2))
console.log(`
Next steps:
1. Verify addresses match: npm run addresses -- ${configPath}
2. Smoke-fund poolAddress with a small intentional amount
3. Exercise consolidate / split / claim before accepting production deposits
`)
+121
View File
@@ -0,0 +1,121 @@
import { createHash } from 'node:crypto'
import { readFileSync, existsSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { hexToBin, binToHex } from '@bitauth/libauth'
import { Contract, ElectrumNetworkProvider, MockNetworkProvider } from 'cashscript'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
export function loadArtifact (name) {
return JSON.parse(readFileSync(join(root, 'artifacts', `${name}.json`), 'utf8'))
}
export function artifactFingerprint (artifact) {
return createHash('sha256')
.update(JSON.stringify({ bytecode: artifact.bytecode, abi: artifact.abi, constructorInputs: artifact.constructorInputs }))
.digest('hex')
}
export function loadDeployConfig (configPath) {
const path = resolve(configPath)
if (!existsSync(path)) {
throw new Error(`Config not found: ${path}. Copy config/deploy.mainnet.example.json to config/deploy.mainnet.json and fill real values.`)
}
const cfg = JSON.parse(readFileSync(path, 'utf8'))
validateConfig(cfg)
return cfg
}
function validateConfig (cfg) {
if (!cfg.nodes || cfg.nodes.length !== 3) {
throw new Error('config.nodes must contain exactly 3 entries (M=3)')
}
for (const [i, node] of cfg.nodes.entries()) {
if (!/^[0-9a-fA-F]{66}$/.test(node.pubkey)) {
throw new Error(`nodes[${i}].pubkey must be 33-byte compressed hex (66 chars)`)
}
if (!/^[0-9a-fA-F]{40}$/.test(node.pkh)) {
throw new Error(`nodes[${i}].pkh must be 20-byte hex (40 chars)`)
}
}
if (!/^[0-9a-fA-F]{40}$/.test(cfg.treasuryPkh)) {
throw new Error('treasuryPkh must be 20-byte hex (40 chars)')
}
if (!Number.isInteger(cfg.minConsolidation) || cfg.minConsolidation < 1) {
throw new Error('minConsolidation must be a positive integer')
}
if (!Number.isInteger(cfg.splitBlockheight) || cfg.splitBlockheight < 1) {
throw new Error('splitBlockheight must be a positive integer (block height)')
}
}
export function createProvider (network) {
if (network === 'mocknet') return new MockNetworkProvider()
return new ElectrumNetworkProvider(network || 'mainnet')
}
export function instantiateContracts (cfg, provider) {
const shareArtifact = loadArtifact('ShareContract')
const poolArtifact = loadArtifact('PoolContract')
const pubs = cfg.nodes.map((n) => hexToBin(n.pubkey))
const shares = cfg.nodes.map((node) =>
new Contract(
shareArtifact,
[...pubs, hexToBin(node.pkh)],
{ provider }
)
)
const pool = new Contract(
poolArtifact,
[
...pubs,
hexToBin(cfg.treasuryPkh),
hexToBin(shares[0].lockingBytecode),
hexToBin(shares[1].lockingBytecode),
hexToBin(shares[2].lockingBytecode),
BigInt(cfg.minConsolidation),
BigInt(cfg.splitBlockheight)
],
{ provider }
)
return {
shares,
pool,
fingerprints: {
ShareContract: artifactFingerprint(shareArtifact),
PoolContract: artifactFingerprint(poolArtifact)
}
}
}
export function buildDeploymentRecord (cfg, { shares, pool, fingerprints }) {
return {
network: cfg.network || 'mainnet',
createdAt: new Date().toISOString(),
minConsolidation: cfg.minConsolidation,
splitBlockheight: cfg.splitBlockheight,
treasuryPkh: cfg.treasuryPkh.toLowerCase(),
nodes: cfg.nodes.map((n, i) => ({
name: n.name || `node${i}`,
pubkey: n.pubkey.toLowerCase(),
pkh: n.pkh.toLowerCase(),
shareAddress: shares[i].address,
shareLockingBytecode: shares[i].lockingBytecode
})),
poolAddress: pool.address,
poolLockingBytecode: pool.lockingBytecode,
artifacts: fingerprints,
constructorNotes: {
M: 3,
K: 2,
deployOrder: 'ShareContracts first, then PoolContract baking share locking bytecodes',
epochModel: 'One splitBlockheight per deploy; next month redeploy + K-of-M migrate'
}
}
}
export { root, binToHex, hexToBin }