deploy-table.ts
1 #!/usr/bin/env bun 2 import { Database } from '@tableland/sdk' 3 import { Wallet, ethers } from 'ethers' 4 import HIDTransport from '@ledgerhq/hw-transport-node-hid' 5 import Eth from '@ledgerhq/hw-app-eth' 6 import { TABLELAND_CONFIG, type TableName, type NetworkName } from '../config' 7 import dotenv from 'dotenv' 8 9 // Try multiple paths for .env 10 const result = dotenv.config({ path: require('path').resolve(process.cwd(), '../.env') }) 11 if (result.error) { 12 dotenv.config({ path: require('path').resolve(process.cwd(), '.env') }) 13 } 14 15 // Custom Ledger Signer that extends ethers AbstractSigner 16 class LedgerSignerV6 extends ethers.AbstractSigner { 17 private eth: Eth 18 private path: string 19 private _address: string | null = null 20 21 constructor(eth: Eth, path: string, provider?: ethers.Provider) { 22 super(provider) 23 this.eth = eth 24 this.path = path 25 } 26 27 async getAddress(): Promise<string> { 28 if (!this._address) { 29 const { address } = await this.eth.getAddress(this.path) 30 this._address = address 31 } 32 return this._address 33 } 34 35 async signTransaction(tx: ethers.TransactionRequest): Promise<string> { 36 const address = await this.getAddress() 37 38 // Populate transaction 39 const populatedTx = await this.populateTransaction(tx) 40 41 // Create transaction for signing 42 const unsignedTx = { 43 to: populatedTx.to, 44 value: populatedTx.value || 0n, 45 data: populatedTx.data || '0x', 46 chainId: populatedTx.chainId, 47 nonce: populatedTx.nonce, 48 gasLimit: populatedTx.gasLimit, 49 maxFeePerGas: populatedTx.maxFeePerGas, 50 maxPriorityFeePerGas: populatedTx.maxPriorityFeePerGas, 51 type: 2 // EIP-1559 52 } 53 54 console.log(`\nā” Please approve the transaction on your Ledger device`) 55 56 // Create ethers Transaction object 57 const ethTx = ethers.Transaction.from(unsignedTx) 58 59 // Sign with Ledger 60 const signature = await this.eth.signTransaction( 61 this.path, 62 ethTx.unsignedSerialized.substring(2), 63 null 64 ) 65 66 // Add signature to transaction 67 ethTx.signature = { 68 r: '0x' + signature.r, 69 s: '0x' + signature.s, 70 v: parseInt(signature.v, 16) 71 } 72 73 return ethTx.serialized 74 } 75 76 async signMessage(message: string | Uint8Array): Promise<string> { 77 const messageHex = typeof message === 'string' 78 ? Buffer.from(message).toString('hex') 79 : Buffer.from(message).toString('hex') 80 81 const signature = await this.eth.signPersonalMessage(this.path, messageHex) 82 return '0x' + signature.r + signature.s + signature.v 83 } 84 85 connect(provider: ethers.Provider): LedgerSignerV6 { 86 return new LedgerSignerV6(this.eth, this.path, provider) 87 } 88 89 // Implement missing abstract method 90 async signTypedData( 91 domain: ethers.TypedDataDomain, 92 types: Record<string, ethers.TypedDataField[]>, 93 value: Record<string, any> 94 ): Promise<string> { 95 throw new Error('signTypedData not implemented for Ledger') 96 } 97 } 98 99 async function deployTable(tableName: TableName, network: NetworkName, useLedger: boolean = false, derivationPath?: string) { 100 const config = TABLELAND_CONFIG.networks[network] 101 const schema = TABLELAND_CONFIG.schemas[tableName] 102 103 console.log(`š Deploying ${tableName} table on ${network}${useLedger ? ' using Ledger' : ''}...`) 104 console.log(`š Chain ID: ${config.chainId}`) 105 console.log(`š RPC: ${config.rpcUrl}`) 106 107 // Setup provider 108 const provider = new ethers.JsonRpcProvider(config.rpcUrl) 109 110 let signer: ethers.Signer 111 let transport: any = null 112 113 try { 114 if (useLedger) { 115 // Default Ethereum derivation path if not provided 116 const path = derivationPath || "m/44'/60'/0'/0/0" 117 console.log(`š Using derivation path: ${path}`) 118 119 // Connect to Ledger 120 console.log(`\nā” Connecting to Ledger...`) 121 transport = await HIDTransport.create() 122 const eth = new Eth(transport) 123 124 // Create Ledger signer 125 signer = new LedgerSignerV6(eth, path, provider) 126 127 // Get address 128 const address = await signer.getAddress() 129 console.log(`ā Connected to Ledger!`) 130 console.log(`š¤ Address: ${address}`) 131 132 // Check balance 133 const balance = await provider.getBalance(address) 134 console.log(`š° Balance: ${ethers.formatEther(balance)} ETH`) 135 136 if (balance === 0n) { 137 throw new Error('Insufficient balance for deployment') 138 } 139 } else { 140 // Use private key signer 141 if (!process.env.PRIVATE_KEY) { 142 throw new Error('PRIVATE_KEY not found in environment variables. Make sure .env file exists and contains PRIVATE_KEY') 143 } 144 145 const privateKey = process.env.PRIVATE_KEY.startsWith('0x') 146 ? process.env.PRIVATE_KEY 147 : `0x${process.env.PRIVATE_KEY}` 148 signer = new Wallet(privateKey, provider) 149 console.log(`š¤ Signer: ${await signer.getAddress()}`) 150 } 151 152 // Create database config 153 const dbConfig: any = { signer } 154 155 // For Base mainnet, explicitly set the registry contract 156 if (network === 'base-mainnet') { 157 dbConfig.baseUrl = config.tablelandHost 158 dbConfig.contract = '0x8268F7Aba0E152B3A853e8CB4Ab9795Ec66c2b6B' 159 } 160 161 const db = new Database(dbConfig) 162 163 if (useLedger) { 164 console.log(`\nš Creating table...`) 165 } 166 167 try { 168 // Create table 169 const { meta } = await db 170 .prepare(`CREATE TABLE ${schema.prefix}_${config.chainId} ${schema.schema}`) 171 .run() 172 173 console.log(`ā ${useLedger ? 'Transaction sent!' : 'Table created!'}`) 174 console.log(`š Table Name: ${meta.txn?.name}`) 175 console.log(`š Transaction: ${meta.txn?.transactionHash}`) 176 177 // Wait for confirmation 178 if (useLedger) { 179 console.log(`\nā³ Waiting for confirmation...`) 180 } 181 try { 182 await meta.txn?.wait() 183 console.log(`ā Transaction confirmed!`) 184 } catch (e) { 185 console.log(`ā ļø Transaction wait timeout (but likely succeeded)`) 186 } 187 188 // Save deployment info 189 const deploymentInfo = { 190 network, 191 chainId: config.chainId, 192 tableName: meta.txn?.name, 193 transactionHash: meta.txn?.transactionHash, 194 deployedAt: new Date().toISOString(), 195 deployedBy: await signer.getAddress(), 196 schema: schema.schema 197 } 198 199 // Save to deployments file 200 const fs = await import('fs') 201 const pathModule = await import('path') 202 const isMainnet = network.includes('mainnet') 203 const deploymentsDir = pathModule.join(import.meta.dir, '../deployments', isMainnet ? 'mainnet' : 'testnet') 204 205 if (!fs.existsSync(deploymentsDir)) { 206 fs.mkdirSync(deploymentsDir, { recursive: true }) 207 } 208 209 const deploymentFile = pathModule.join(deploymentsDir, `${network.replace('-', '_')}.json`) 210 let deployments: any = {} 211 212 if (fs.existsSync(deploymentFile)) { 213 deployments = JSON.parse(fs.readFileSync(deploymentFile, 'utf8')) 214 } 215 216 // Save table info 217 if (!deployments.tables) deployments.tables = {} 218 deployments.tables[tableName] = deploymentInfo 219 deployments.lastUpdated = new Date().toISOString() 220 221 fs.writeFileSync(deploymentFile, JSON.stringify(deployments, null, 2)) 222 console.log(`\nš¾ Saved deployment to: ${deploymentFile}`) 223 224 console.log(`\nš Deployment Summary:`) 225 console.log(JSON.stringify(deploymentInfo, null, 2)) 226 227 } catch (error: any) { 228 console.error(`\nā Deployment failed:`, error.message) 229 if (useLedger && error.stack) { 230 console.error(`Stack trace:`, error.stack) 231 } 232 if (useLedger && (error.message.includes('Ledger') || error.message.includes('Transport'))) { 233 console.error(`\nš” Troubleshooting tips:`) 234 console.error(` - Make sure Ledger is connected and unlocked`) 235 console.error(` - Ensure Ethereum app is open on the device`) 236 console.error(` - Enable "Contract data" in Ethereum app settings`) 237 console.error(` - Try a different USB port or cable`) 238 } 239 process.exit(1) 240 } 241 } finally { 242 // Close transport 243 if (transport) { 244 await transport.close() 245 } 246 } 247 } 248 249 // CLI usage 250 const args = process.argv.slice(2) 251 252 // Parse flags 253 const ledgerIndex = args.indexOf('--ledger') 254 const useLedger = ledgerIndex !== -1 255 if (useLedger) { 256 args.splice(ledgerIndex, 1) 257 } 258 259 // Parse derivation path if provided 260 const pathIndex = args.indexOf('--path') 261 let derivationPath: string | undefined 262 if (pathIndex !== -1 && args[pathIndex + 1]) { 263 derivationPath = args[pathIndex + 1] 264 args.splice(pathIndex, 2) 265 } 266 267 if (args.length < 2) { 268 console.error('Usage: bun run deploy-table.ts <table-name> <network> [--ledger] [--path <derivation-path>]') 269 console.error('Tables:', Object.keys(TABLELAND_CONFIG.schemas).join(', ')) 270 console.error('Networks:', Object.keys(TABLELAND_CONFIG.networks).join(', ')) 271 console.error('\nExamples:') 272 console.error(' bun run deploy-table.ts songs base-mainnet') 273 console.error(' bun run deploy-table.ts songs base-mainnet --ledger') 274 console.error(' bun run deploy-table.ts songs base-mainnet --ledger --path "m/44\'/60\'/0\'/0/1"') 275 process.exit(1) 276 } 277 278 const tableName = args[0] as TableName 279 const network = args[1] as NetworkName 280 281 if (!TABLELAND_CONFIG.schemas[tableName]) { 282 console.error(`ā Unknown table: ${tableName}`) 283 process.exit(1) 284 } 285 286 if (!TABLELAND_CONFIG.networks[network]) { 287 console.error(`ā Unknown network: ${network}`) 288 process.exit(1) 289 } 290 291 if (!useLedger && !process.env.PRIVATE_KEY) { 292 console.error('ā PRIVATE_KEY not found in environment variables. Make sure .env file exists and contains PRIVATE_KEY') 293 process.exit(1) 294 } 295 296 deployTable(tableName, network, useLedger, derivationPath) 297 .then(() => { 298 console.log('\n⨠Deployment complete!') 299 process.exit(0) 300 }) 301 .catch((error) => { 302 console.error('Fatal error:', error) 303 process.exit(1) 304 })