/ tableland / deploy / deploy-table.ts
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    })