#!/usr/bin/env node // Independent, devnet-only recovery client. Keep the program ID from a trusted release. import { readFile } from 'node:fs/promises'; import { parseArgs } from 'node:util'; import { createInterface } from 'node:readline/promises'; import { Keypair, PublicKey } from '@solana/web3.js'; import { buildTransaction, checkDeployment, listChainGrants, openDevnet, readGrant, revokeAllIx, revokeIx, sendPrepared } from '../lib/registry.mjs'; const help = [ 'Solbound independent devnet revocation tool', 'List: node tools/revoke.mjs --program --vault
', 'Review one: add --grant . Review all: add --all.', 'Sign locally: also pass --keypair --execute', 'Leave cold wallet keys on their device. This CLI signs with an existing local devnet file only.', ].join('\n'); async function main(){ const {values:v}=parseArgs({options:{ program:{type:'string'},vault:{type:'string'},grant:{type:'string'},all:{type:'boolean'}, keypair:{type:'string'},execute:{type:'boolean'},help:{type:'boolean'},rpc:{type:'string'} }}); if(v.help){console.log(help);return;} if(!v.program || !v.vault || (v.all && v.grant)) throw new Error(help); const client=openDevnet(v.program,v.rpc),vault=new PublicKey(v.vault); await checkDeployment(client); if(!v.all && !v.grant){ if(v.execute || v.keypair) throw new Error('Choose --grant or --all before signing.'); console.table(await listChainGrants(client,v.vault));return; } if(v.grant){ const id=new PublicKey(v.grant); const record=readGrant(client.program,id,await client.connection.getAccountInfo(id,'finalized')); if(record.owner!==v.vault) throw new Error('The grant belongs to a different vault.'); console.log('Delegate:',record.delegate,'Scope:',record.scope,record.asset??''); } const ix=v.all?revokeAllIx(client.program,vault):revokeIx(client.program,vault,v.grant); const prepared=await buildTransaction(client,v.vault,ix); console.log('Network: devnet\nProgram:',v.program,'\nVault:',v.vault,'\nRevoke:',v.all?'ALL grants from this vault':v.grant,'\nNetwork fee:',prepared.feeLamports,'lamports'); if(!v.execute){ console.log('Unsigned transaction (base64, valid until block height '+prepared.latest.lastValidBlockHeight+'):\n'+prepared.tx.serialize({requireAllSignatures:false,verifySignatures:false}).toString('base64')); return; } if(!v.keypair || !process.stdin.isTTY) throw new Error('Local signing requires an existing devnet keypair file and interactive confirmation.'); const prompt=createInterface({input:process.stdin,output:process.stdout}); let answer;try{answer=await prompt.question('Type REVOKE to submit this devnet transaction: ');}finally{prompt.close();} if(answer!=='REVOKE') throw new Error('Cancelled.'); const bytes=Uint8Array.from(JSON.parse(await readFile(v.keypair,'utf8'))); let key;try{key=Keypair.fromSecretKey(bytes);}finally{bytes.fill(0);} if(!key.publicKey.equals(vault)) throw new Error('Signing key does not match the vault.'); const receipt=await sendPrepared(client,prepared,tx=>{tx.sign(key);return tx;}); console.log('Finalized:',receipt.signature); } main().catch(err=>{console.error(err.message);process.exitCode=1;});