|
#!/usr/bin/env node |
|
/* |
|
* Register a builder identity with Vana Data Portability. |
|
* |
|
* This script is deliberately a small protocol client, rather than a wrapper |
|
* around Account. A builder owns the EOA private key it uses for registration; |
|
* it must retain that key as VANA_PRIVATE_KEY to use the registered app later. |
|
* |
|
* Protocol source: https://github.com/vana-com/data-gateway/blob/dev/api/v1/builders.ts |
|
* Mainnet RPC: https://dp-rpc.vana.org |
|
* Moksha testnet RPC: https://dp-rpc-dev.vana.org |
|
*/ |
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; |
|
|
|
export const NETWORKS = { |
|
mainnet: { |
|
chainId: 1480, |
|
rpcUrl: "https://dp-rpc.vana.org", |
|
}, |
|
testnet: { |
|
chainId: 14800, |
|
rpcUrl: "https://dp-rpc-dev.vana.org", |
|
}, |
|
}; |
|
|
|
// The builders endpoint verifies this EIP-712 domain against the canonical |
|
// DataPortabilityGrantees contract. The address is shared by mainnet/Moksha; |
|
// chainId prevents a signature from being replayed across environments. |
|
export const DATA_PORTABILITY_GRANTEES = |
|
"0x8325C0A0948483EdA023A1A2Fd895e62C5131234"; |
|
|
|
export const BUILDER_REGISTRATION_TYPES = { |
|
BuilderRegistration: [ |
|
{ name: "ownerAddress", type: "address" }, |
|
{ name: "granteeAddress", type: "address" }, |
|
{ name: "publicKey", type: "string" }, |
|
{ name: "appUrl", type: "string" }, |
|
], |
|
}; |
|
|
|
function usage(error) { |
|
const message = ` |
|
Usage: |
|
npm run register -- --app-url https://your-app.example [options] |
|
|
|
Options: |
|
--network mainnet|testnet Target network (default: mainnet) |
|
--private-key 0x... Existing builder key (or set VANA_PRIVATE_KEY) |
|
--dry-run Sign and print the request; do not register it |
|
--confirm-mainnet Required before a mainnet registration is sent |
|
--json Emit machine-readable output |
|
--help Show this help |
|
`.trim(); |
|
if (error) throw new Error(`${error}\n\n${message}`); |
|
console.log(message); |
|
} |
|
|
|
export function parseArgs(args) { |
|
const options = { network: "mainnet", dryRun: false, json: false }; |
|
for (let index = 0; index < args.length; index += 1) { |
|
const argument = args[index]; |
|
if (argument === "--help") options.help = true; |
|
else if (argument === "--dry-run") options.dryRun = true; |
|
else if (argument === "--confirm-mainnet") options.confirmMainnet = true; |
|
else if (argument === "--json") options.json = true; |
|
else if (["--app-url", "--network", "--private-key"].includes(argument)) { |
|
const value = args[index + 1]; |
|
if (!value || value.startsWith("--")) usage(`Missing value for ${argument}.`); |
|
options[{ "--app-url": "appUrl", "--network": "network", "--private-key": "privateKey" }[argument]] = value; |
|
index += 1; |
|
} else usage(`Unknown argument: ${argument}`); |
|
} |
|
return options; |
|
} |
|
|
|
function validateAppUrl(value) { |
|
try { |
|
const url = new URL(value); |
|
if (!['http:', 'https:'].includes(url.protocol)) throw new Error(); |
|
return url.href; |
|
} catch { |
|
usage("--app-url must be an absolute http(s) URL."); |
|
} |
|
} |
|
|
|
function output(result, json) { |
|
if (json) return console.log(JSON.stringify(result, null, 2)); |
|
console.log(`Network: ${result.network}`); |
|
console.log(`RPC: ${result.rpcUrl}`); |
|
console.log(`Builder address: ${result.builderAddress}`); |
|
console.log(`App URL: ${result.request.appUrl}`); |
|
if (result.generatedPrivateKey) { |
|
console.log("\nA fresh builder identity was generated. Store this now; it is not saved by the script:"); |
|
console.log(`VANA_PRIVATE_KEY=${result.generatedPrivateKey}`); |
|
} |
|
if (result.dryRun) console.log("\nDry run only: no builder was registered."); |
|
else console.log(`\nRegistered successfully (HTTP ${result.status}).`); |
|
} |
|
|
|
export async function buildRegistration({ appUrl, networkName, privateKey }) { |
|
const network = NETWORKS[networkName]; |
|
if (!network) usage("--network must be mainnet or testnet."); |
|
const generatedPrivateKey = privateKey ? undefined : generatePrivateKey(); |
|
const account = privateKeyToAccount(privateKey ?? generatedPrivateKey); |
|
const request = { |
|
ownerAddress: account.address, |
|
granteeAddress: account.address, |
|
publicKey: account.publicKey, |
|
appUrl: validateAppUrl(appUrl), |
|
}; |
|
const signature = await account.signTypedData({ |
|
domain: { |
|
name: "Vana Data Portability", |
|
version: "1", |
|
chainId: network.chainId, |
|
verifyingContract: DATA_PORTABILITY_GRANTEES, |
|
}, |
|
types: BUILDER_REGISTRATION_TYPES, |
|
primaryType: "BuilderRegistration", |
|
message: request, |
|
}); |
|
return { network, request, signature, generatedPrivateKey }; |
|
} |
|
|
|
export async function registerBuilder({ appUrl, networkName = "mainnet", privateKey, dryRun = false, confirmMainnet = false }) { |
|
const registration = await buildRegistration({ appUrl, networkName, privateKey }); |
|
if (networkName === "mainnet" && !dryRun && !confirmMainnet) { |
|
usage("Refusing to write to mainnet without --confirm-mainnet. Use --dry-run first to inspect the request."); |
|
} |
|
if (dryRun) return { ...registration, dryRun: true }; |
|
|
|
const response = await fetch(`${registration.network.rpcUrl}/v1/builders`, { |
|
method: "POST", |
|
headers: { |
|
"content-type": "application/json", |
|
authorization: `Web3Signed ${registration.signature}`, |
|
}, |
|
body: JSON.stringify(registration.request), |
|
}); |
|
const bodyText = await response.text(); |
|
let body; |
|
try { body = bodyText ? JSON.parse(bodyText) : null; } catch { body = { raw: bodyText }; } |
|
if (response.status !== 201) { |
|
throw new Error(`Data Portability RPC rejected the registration (HTTP ${response.status}): ${JSON.stringify(body)}`); |
|
} |
|
return { ...registration, status: response.status, body }; |
|
} |
|
|
|
async function main() { |
|
const options = parseArgs(process.argv.slice(2)); |
|
if (options.help) return usage(); |
|
if (!options.appUrl) usage("--app-url is required."); |
|
const result = await registerBuilder({ |
|
appUrl: options.appUrl, |
|
networkName: options.network, |
|
privateKey: options.privateKey ?? process.env.VANA_PRIVATE_KEY, |
|
dryRun: options.dryRun, |
|
confirmMainnet: options.confirmMainnet, |
|
}); |
|
output({ |
|
network: options.network, |
|
rpcUrl: result.network.rpcUrl, |
|
builderAddress: result.request.granteeAddress, |
|
request: result.request, |
|
generatedPrivateKey: result.generatedPrivateKey, |
|
dryRun: result.dryRun, |
|
status: result.status, |
|
response: result.body, |
|
}, options.json); |
|
} |
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) { |
|
main().catch((error) => { |
|
console.error(`Error: ${error.message}`); |
|
process.exitCode = 1; |
|
}); |
|
} |