[{"data":1,"prerenderedAt":10034},["ShallowReactive",2],{"/articles/the-electron-experience":3},{"id":4,"title":5,"body":6,"date":10019,"description":10020,"extension":10021,"head":10022,"image":16,"meta":10023,"navigation":188,"ogImage":10022,"path":10024,"readingTime":10025,"robots":10022,"schemaOrg":10022,"seo":10026,"sitemap":10027,"stem":10028,"tags":10029,"__hash__":10033},"articles/articles/00.the-electron-experience.md","Building Desktop Apps on The Solana Blockchain with Electron",{"type":7,"value":8,"toc":10004},"minimark",[9,17,34,39,54,68,88,97,106,115,119,134,359,374,379,382,395,406,1320,1337,1340,1348,1351,1366,1508,1519,2176,2179,2193,2196,2225,2228,2232,2235,2246,2249,2252,2401,2408,2414,2423,2426,2429,2801,2808,2814,3147,3150,3163,3170,3173,3415,3418,3425,3574,3585,3798,3807,3810,3820,3823,3830,4045,4056,4059,4066,4457,4463,4474,4477,4488,4659,4665,5454,5463,5901,5907,5920,5923,5927,5930,5933,5954,5968,5988,6012,6021,6024,6037,6040,6046,6049,6873,6884,6895,6922,6927,6933,6938,7209,7212,7216,7219,7222,7225,7232,7568,7578,8131,8135,8138,8141,8150,8153,8157,8168,8171,8507,8510,8520,8612,8626,8630,8633,8636,8639,8787,8797,8808,8824,8944,8964,8968,8974,8983,8993,9003,9879,9890,9981,9984,9987,10000],[10,11,12],"p",{},[13,14],"img",{"alt":15,"src":16},"preview","/articles/the-solana-experince.png",[10,18,19,20,27,28,33],{},"Around 3 weeks ago at the time of writing this post, I was asked by a friend \"Can you build me a trading bot that can trade meme coins?\", I then thought to myself \"well why not, how hard can that possibly be?, if I keep it simple and avoid over engineering this\", so I asked what capabilities you want your bot to have, he said \"just the basics, buy/sell and if you can add a volume bot to it also\". Well, on the surface especially when you're not familiar with blockchain tech and its ecosystem that sounds easy, and a great learning experience, so I agreed. Guess what? I was brutally wrong. And only realized that when I started researching this whole topic. You can find the final App in its ",[21,22,26],"a",{"href":23,"rel":24},"https://solana.riavzon.com/",[25],"nofollow","website"," or ",[21,29,32],{"href":30,"rel":31},"https://github.com/Sergo706/solana-bots",[25],"Github",".",[35,36,38],"h2",{"id":37},"researching","Researching",[10,40,41,42,47,48,53],{},"I started my research on what meme coins even are, how they are created, and how one can buy or sell these. What I found were mostly existing trading platforms, that all point to creating/importing wallets, adding SOL to them and start trading. So I searched \"what sols are\", and discovered ",[21,43,46],{"href":44,"rel":45},"https://solana.com/",[25],"Solana",", and that sols are a cryptocurrency, and Solana is the primary blockchain of these, and that ",[21,49,52],{"href":50,"rel":51},"https://solana.com/learn/what-is-a-wallet",[25],"wallets"," are the thing you keep these sols in and trade with, and they consist of key pairs of public key (the address you use to receive funds) and a private key (the secret key you use to sign transactions and send funds). So I figured out that my bots, are actually wallets, and the thing that powers them is SOL.",[10,55,56,57,62,63,33],{},"After exploring the platform's docs, and examples, I started experimenting creating wallets, adding SOL to them with Airdrops via the public rpc ",[21,58,61],{"href":59,"rel":60},"https://solana.com/docs/rpc",[25],"endpoint"," (specifically the devnet endpoint), and sending them around between wallets with ",[21,64,67],{"href":65,"rel":66},"https://solana.com/docs/core/transactions",[25],"transactions",[10,69,70,71,76,77,82,83,33],{},"And that \"meme coins\" are ",[21,72,75],{"href":73,"rel":74},"https://solana.com/docs/tokens/basics/create-mint",[25],"Token mints"," with ",[21,78,81],{"href":79,"rel":80},"https://solana.com/docs/tokens/metaplex",[25],"metaplex metadata",". You use your wallets to sell and receive tokens, from these mints, with a DEX such as ",[21,84,87],{"href":85,"rel":86},"https://raydium.io/",[25],"Raydium",[10,89,90,91,96],{},"At this point I got almost all of that already figured out, what I realized more about this ecosystem, is the scam potential it has over people that have absolutely no idea, how easy it is, to pull off ",[21,92,95],{"href":93,"rel":94},"https://en.wikipedia.org/wiki/Exit_scam",[25],"rug pulls",", manipulate pools and control your coins as a developer. Which most of this ecosystem is built around this idea \"it's just part of the game\", and completely accept that. What I found out more, is that there are huge enterprises who know all that and build their entire business model around these scams at scale.",[10,98,99,100,105],{},"With the research done, I started creating the dev environment, and with the idea of keeping it simple but usable and easy enough for my friend to use, I picked ",[21,101,104],{"href":102,"rel":103},"https://developers.jup.ag/",[25],"Jupiter"," as the DEX aggregator to easily get the best prices and quotes, and avoid complex math and logic, they have generous rate limits (at the time of writing) 1/rps and unlimited usage, which is more than enough for my friend. I told him \"it will be a cli without any UI at first, see how you get around that, and if you find it challenging, I'll create you a desktop app\".",[10,107,108,109,114],{},"Why not? I have a good knowledge of Vue, React, and Electron shouldn't be that hard to navigate, as it's just web technologies, and their build tool, ",[21,110,113],{"href":111,"rel":112},"https://www.electronforge.io/",[25],"Electron forge"," can be used with Vite, React, Vue and TypeScript, plus their docs state \"It combines many single-purpose packages to create a full build pipeline that works out of the box\" so it should be easy right?...",[35,116,118],{"id":117},"development","Development",[10,120,121,122,127,128,133],{},"Starting with the CLI, I set up my ",[21,123,126],{"href":124,"rel":125},"https://github.com/Sergo706/utils/blob/main/eslint/strict.ts.config.ts",[25],"eslint config"," installed ",[21,129,132],{"href":130,"rel":131},"https://github.com/unjs/citty",[25],"citty"," as the CLI builder, drizzle with better-sqlite3 for wallets storage, and started the development. I defined the schema to be:",[135,136,142],"pre",{"className":137,"code":138,"filename":139,"language":140,"meta":141,"style":141},"language-ts shiki shiki-themes github-light github-dark github-dark","import { blob, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { sql } from \"drizzle-orm\";\n\nexport const walletTable = sqliteTable('wallets', {\n    id: integer('id', {mode: 'number'}).primaryKey({ autoIncrement: true }),\n    publicAddress: text('public_address').unique(),\n    isMainWallet: integer('is_main_wallet', {mode: 'boolean'}),\n    privateKeyEncrypted: blob('private_key_encrypted').notNull(),\n    createdAt: integer('created_at').notNull().default(sql`(cast(unixepoch() as int))`),\n});\n","wallets.ts","ts","",[143,144,145,168,183,190,219,254,277,298,319,353],"code",{"__ignoreMap":141},[146,147,150,154,158,161,165],"span",{"class":148,"line":149},"line",1,[146,151,153],{"class":152},"so5gQ","import",[146,155,157],{"class":156},"slsVL"," { blob, integer, sqliteTable, text } ",[146,159,160],{"class":152},"from",[146,162,164],{"class":163},"sfrk1"," \"drizzle-orm/sqlite-core\"",[146,166,167],{"class":156},";\n",[146,169,171,173,176,178,181],{"class":148,"line":170},2,[146,172,153],{"class":152},[146,174,175],{"class":156}," { sql } ",[146,177,160],{"class":152},[146,179,180],{"class":163}," \"drizzle-orm\"",[146,182,167],{"class":156},[146,184,186],{"class":148,"line":185},3,[146,187,189],{"emptyLinePlaceholder":188},true,"\n",[146,191,193,196,199,203,206,210,213,216],{"class":148,"line":192},4,[146,194,195],{"class":152},"export",[146,197,198],{"class":152}," const",[146,200,202],{"class":201},"suiK_"," walletTable",[146,204,205],{"class":152}," =",[146,207,209],{"class":208},"shcOC"," sqliteTable",[146,211,212],{"class":156},"(",[146,214,215],{"class":163},"'wallets'",[146,217,218],{"class":156},", {\n",[146,220,222,225,228,230,233,236,239,242,245,248,251],{"class":148,"line":221},5,[146,223,224],{"class":156},"    id: ",[146,226,227],{"class":208},"integer",[146,229,212],{"class":156},[146,231,232],{"class":163},"'id'",[146,234,235],{"class":156},", {mode: ",[146,237,238],{"class":163},"'number'",[146,240,241],{"class":156},"}).",[146,243,244],{"class":208},"primaryKey",[146,246,247],{"class":156},"({ autoIncrement: ",[146,249,250],{"class":201},"true",[146,252,253],{"class":156}," }),\n",[146,255,257,260,263,265,268,271,274],{"class":148,"line":256},6,[146,258,259],{"class":156},"    publicAddress: ",[146,261,262],{"class":208},"text",[146,264,212],{"class":156},[146,266,267],{"class":163},"'public_address'",[146,269,270],{"class":156},").",[146,272,273],{"class":208},"unique",[146,275,276],{"class":156},"(),\n",[146,278,280,283,285,287,290,292,295],{"class":148,"line":279},7,[146,281,282],{"class":156},"    isMainWallet: ",[146,284,227],{"class":208},[146,286,212],{"class":156},[146,288,289],{"class":163},"'is_main_wallet'",[146,291,235],{"class":156},[146,293,294],{"class":163},"'boolean'",[146,296,297],{"class":156},"}),\n",[146,299,301,304,307,309,312,314,317],{"class":148,"line":300},8,[146,302,303],{"class":156},"    privateKeyEncrypted: ",[146,305,306],{"class":208},"blob",[146,308,212],{"class":156},[146,310,311],{"class":163},"'private_key_encrypted'",[146,313,270],{"class":156},[146,315,316],{"class":208},"notNull",[146,318,276],{"class":156},[146,320,322,325,327,329,332,334,336,339,342,344,347,350],{"class":148,"line":321},9,[146,323,324],{"class":156},"    createdAt: ",[146,326,227],{"class":208},[146,328,212],{"class":156},[146,330,331],{"class":163},"'created_at'",[146,333,270],{"class":156},[146,335,316],{"class":208},[146,337,338],{"class":156},"().",[146,340,341],{"class":208},"default",[146,343,212],{"class":156},[146,345,346],{"class":208},"sql",[146,348,349],{"class":163},"`(cast(unixepoch() as int))`",[146,351,352],{"class":156},"),\n",[146,354,356],{"class":148,"line":355},10,[146,357,358],{"class":156},"});\n",[10,360,361,362,365,366,370,371,373],{},"Nothing crazy, except that the ",[143,363,364],{},"isMainWallet"," column filters a wallet as a main one, only a single wallet exists in the system that is flagged as main. This wallet is directly used to distribute funds to ",[367,368,369],"em",{},"sub-wallets"," which their ",[143,372,364],{}," is 0. And to drain sub-wallets back to main.",[375,376,378],"h3",{"id":377},"creating-wallets","Creating Wallets",[10,380,381],{},"Obviously, the private key of a wallet is a sensitive field, and should be kept secret, I needed a way to keep that field:",[383,384,385,389,392],"ul",{},[386,387,388],"li",{},"Encrypted when a new wallet is stored",[386,390,391],{},"Decrypted on demand when a sensitive action is requested",[386,393,394],{},"Needs to be simple enough for a non-technical user to use and navigate",[10,396,397,398,401,402,405],{},"So I thought, why not use ",[143,399,400],{},"aes-256-gcm","? It gives you data integrity (eg. authentication) and I can ask the user for a \"password\", which is hashed and salted with ",[143,403,404],{},"scrypt"," to derive a cryptographic key. This derived key is then fed directly into the AES algorithm to encrypt the wallet's private key. Then, for the decryption function I can ask for that same password with the encrypted value. So that's what I ended up with:",[135,407,410],{"className":137,"code":408,"filename":409,"language":140,"meta":141,"style":141},"const SCRYPT_CONFIG = {\n  keyLen: 32,\n  saltLen: 32,\n  params: { N: 16384, r: 8, p: 1 }\n};\n\n\nexport const encryptPrivateKey = (privateKey: Uint8Array, password: string): Buffer => {\n  try {\n    const iv = crypto.randomBytes(12);\n    const salt = crypto.randomBytes(SCRYPT_CONFIG.saltLen);\n    const key = crypto.scryptSync(password, salt, SCRYPT_CONFIG.keyLen, SCRYPT_CONFIG.params);\n    \n    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);\n    const encrypted = Buffer.concat([cipher.update(privateKey), cipher.final()]);\n    const tag = cipher.getAuthTag();\n\n    return Buffer.concat([salt, iv, tag, encrypted]);\n  } catch (err) {\n    throw new Error(`Encryption failed: ${(err as Error).message}`);\n  }\n};\n\nexport const decryptPrivateKey = (encrypted: Buffer, password: string): Uint8Array => {\n  try {\n\n    const saltLen = SCRYPT_CONFIG.saltLen;\n    const ivLen = 12; \n    const tagLen = 16;\n    const minLength = saltLen + ivLen + tagLen;\n\n    if (encrypted.length \u003C minLength) {\n      throw new Error('Invalid encrypted data format');\n    }\n\n    let offset = 0;\n    const salt = encrypted.subarray(offset, offset += saltLen);\n    const iv = encrypted.subarray(offset, offset += ivLen);\n    const tag = encrypted.subarray(offset, offset += tagLen);\n    const ciphertext = encrypted.subarray(offset);\n\n    const key = crypto.scryptSync(password, salt, SCRYPT_CONFIG.keyLen, SCRYPT_CONFIG.params);\n    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);\n    decipher.setAuthTag(tag);\n\n    return new Uint8Array(Buffer.concat([\n      decipher.update(ciphertext),\n      decipher.final()\n    ]));\n  } catch (error: unknown) {\n    const err = error instanceof Error ? error : null;\n    const code = (error as NodeJS.ErrnoException).code;\n    if (code === 'ERR_CRYPTO_OPERATION_FAILED' || err?.message.toLowerCase().includes('unsupported state')) {\n      throw new Error('Incorrect password or data');\n    }\n    consola.log(`Decryption failure`);\n    throw error;\n  }\n};\n","utils.ts",[143,411,412,425,436,445,468,473,477,481,529,536,560,582,610,616,639,670,689,694,707,719,756,762,767,772,811,818,823,838,854,869,893,898,916,933,939,944,961,985,1005,1025,1042,1047,1070,1091,1103,1108,1125,1136,1146,1152,1172,1202,1229,1265,1281,1286,1302,1310,1315],{"__ignoreMap":141},[146,413,414,417,420,422],{"class":148,"line":149},[146,415,416],{"class":152},"const",[146,418,419],{"class":201}," SCRYPT_CONFIG",[146,421,205],{"class":152},[146,423,424],{"class":156}," {\n",[146,426,427,430,433],{"class":148,"line":170},[146,428,429],{"class":156},"  keyLen: ",[146,431,432],{"class":201},"32",[146,434,435],{"class":156},",\n",[146,437,438,441,443],{"class":148,"line":185},[146,439,440],{"class":156},"  saltLen: ",[146,442,432],{"class":201},[146,444,435],{"class":156},[146,446,447,450,453,456,459,462,465],{"class":148,"line":192},[146,448,449],{"class":156},"  params: { N: ",[146,451,452],{"class":201},"16384",[146,454,455],{"class":156},", r: ",[146,457,458],{"class":201},"8",[146,460,461],{"class":156},", p: ",[146,463,464],{"class":201},"1",[146,466,467],{"class":156}," }\n",[146,469,470],{"class":148,"line":221},[146,471,472],{"class":156},"};\n",[146,474,475],{"class":148,"line":256},[146,476,189],{"emptyLinePlaceholder":188},[146,478,479],{"class":148,"line":279},[146,480,189],{"emptyLinePlaceholder":188},[146,482,483,485,487,490,492,495,499,502,505,508,511,513,516,519,521,524,527],{"class":148,"line":300},[146,484,195],{"class":152},[146,486,198],{"class":152},[146,488,489],{"class":208}," encryptPrivateKey",[146,491,205],{"class":152},[146,493,494],{"class":156}," (",[146,496,498],{"class":497},"sQHwn","privateKey",[146,500,501],{"class":152},":",[146,503,504],{"class":208}," Uint8Array",[146,506,507],{"class":156},", ",[146,509,510],{"class":497},"password",[146,512,501],{"class":152},[146,514,515],{"class":201}," string",[146,517,518],{"class":156},")",[146,520,501],{"class":152},[146,522,523],{"class":208}," Buffer",[146,525,526],{"class":152}," =>",[146,528,424],{"class":156},[146,530,531,534],{"class":148,"line":321},[146,532,533],{"class":152},"  try",[146,535,424],{"class":156},[146,537,538,541,544,546,549,552,554,557],{"class":148,"line":355},[146,539,540],{"class":152},"    const",[146,542,543],{"class":201}," iv",[146,545,205],{"class":152},[146,547,548],{"class":156}," crypto.",[146,550,551],{"class":208},"randomBytes",[146,553,212],{"class":156},[146,555,556],{"class":201},"12",[146,558,559],{"class":156},");\n",[146,561,563,565,568,570,572,574,576,579],{"class":148,"line":562},11,[146,564,540],{"class":152},[146,566,567],{"class":201}," salt",[146,569,205],{"class":152},[146,571,548],{"class":156},[146,573,551],{"class":208},[146,575,212],{"class":156},[146,577,578],{"class":201},"SCRYPT_CONFIG",[146,580,581],{"class":156},".saltLen);\n",[146,583,585,587,590,592,594,597,600,602,605,607],{"class":148,"line":584},12,[146,586,540],{"class":152},[146,588,589],{"class":201}," key",[146,591,205],{"class":152},[146,593,548],{"class":156},[146,595,596],{"class":208},"scryptSync",[146,598,599],{"class":156},"(password, salt, ",[146,601,578],{"class":201},[146,603,604],{"class":156},".keyLen, ",[146,606,578],{"class":201},[146,608,609],{"class":156},".params);\n",[146,611,613],{"class":148,"line":612},13,[146,614,615],{"class":156},"    \n",[146,617,619,621,624,626,628,631,633,636],{"class":148,"line":618},14,[146,620,540],{"class":152},[146,622,623],{"class":201}," cipher",[146,625,205],{"class":152},[146,627,548],{"class":156},[146,629,630],{"class":208},"createCipheriv",[146,632,212],{"class":156},[146,634,635],{"class":163},"'aes-256-gcm'",[146,637,638],{"class":156},", key, iv);\n",[146,640,642,644,647,649,652,655,658,661,664,667],{"class":148,"line":641},15,[146,643,540],{"class":152},[146,645,646],{"class":201}," encrypted",[146,648,205],{"class":152},[146,650,651],{"class":156}," Buffer.",[146,653,654],{"class":208},"concat",[146,656,657],{"class":156},"([cipher.",[146,659,660],{"class":208},"update",[146,662,663],{"class":156},"(privateKey), cipher.",[146,665,666],{"class":208},"final",[146,668,669],{"class":156},"()]);\n",[146,671,673,675,678,680,683,686],{"class":148,"line":672},16,[146,674,540],{"class":152},[146,676,677],{"class":201}," tag",[146,679,205],{"class":152},[146,681,682],{"class":156}," cipher.",[146,684,685],{"class":208},"getAuthTag",[146,687,688],{"class":156},"();\n",[146,690,692],{"class":148,"line":691},17,[146,693,189],{"emptyLinePlaceholder":188},[146,695,697,700,702,704],{"class":148,"line":696},18,[146,698,699],{"class":152},"    return",[146,701,651],{"class":156},[146,703,654],{"class":208},[146,705,706],{"class":156},"([salt, iv, tag, encrypted]);\n",[146,708,710,713,716],{"class":148,"line":709},19,[146,711,712],{"class":156},"  } ",[146,714,715],{"class":152},"catch",[146,717,718],{"class":156}," (err) {\n",[146,720,722,725,728,731,733,736,738,741,744,746,748,751,754],{"class":148,"line":721},20,[146,723,724],{"class":152},"    throw",[146,726,727],{"class":152}," new",[146,729,730],{"class":208}," Error",[146,732,212],{"class":156},[146,734,735],{"class":163},"`Encryption failed: ${",[146,737,212],{"class":163},[146,739,740],{"class":156},"err",[146,742,743],{"class":152}," as",[146,745,730],{"class":208},[146,747,270],{"class":163},[146,749,750],{"class":156},"message",[146,752,753],{"class":163},"}`",[146,755,559],{"class":156},[146,757,759],{"class":148,"line":758},21,[146,760,761],{"class":156},"  }\n",[146,763,765],{"class":148,"line":764},22,[146,766,472],{"class":156},[146,768,770],{"class":148,"line":769},23,[146,771,189],{"emptyLinePlaceholder":188},[146,773,775,777,779,782,784,786,789,791,793,795,797,799,801,803,805,807,809],{"class":148,"line":774},24,[146,776,195],{"class":152},[146,778,198],{"class":152},[146,780,781],{"class":208}," decryptPrivateKey",[146,783,205],{"class":152},[146,785,494],{"class":156},[146,787,788],{"class":497},"encrypted",[146,790,501],{"class":152},[146,792,523],{"class":208},[146,794,507],{"class":156},[146,796,510],{"class":497},[146,798,501],{"class":152},[146,800,515],{"class":201},[146,802,518],{"class":156},[146,804,501],{"class":152},[146,806,504],{"class":208},[146,808,526],{"class":152},[146,810,424],{"class":156},[146,812,814,816],{"class":148,"line":813},25,[146,815,533],{"class":152},[146,817,424],{"class":156},[146,819,821],{"class":148,"line":820},26,[146,822,189],{"emptyLinePlaceholder":188},[146,824,826,828,831,833,835],{"class":148,"line":825},27,[146,827,540],{"class":152},[146,829,830],{"class":201}," saltLen",[146,832,205],{"class":152},[146,834,419],{"class":201},[146,836,837],{"class":156},".saltLen;\n",[146,839,841,843,846,848,851],{"class":148,"line":840},28,[146,842,540],{"class":152},[146,844,845],{"class":201}," ivLen",[146,847,205],{"class":152},[146,849,850],{"class":201}," 12",[146,852,853],{"class":156},"; \n",[146,855,857,859,862,864,867],{"class":148,"line":856},29,[146,858,540],{"class":152},[146,860,861],{"class":201}," tagLen",[146,863,205],{"class":152},[146,865,866],{"class":201}," 16",[146,868,167],{"class":156},[146,870,872,874,877,879,882,885,888,890],{"class":148,"line":871},30,[146,873,540],{"class":152},[146,875,876],{"class":201}," minLength",[146,878,205],{"class":152},[146,880,881],{"class":156}," saltLen ",[146,883,884],{"class":152},"+",[146,886,887],{"class":156}," ivLen ",[146,889,884],{"class":152},[146,891,892],{"class":156}," tagLen;\n",[146,894,896],{"class":148,"line":895},31,[146,897,189],{"emptyLinePlaceholder":188},[146,899,901,904,907,910,913],{"class":148,"line":900},32,[146,902,903],{"class":152},"    if",[146,905,906],{"class":156}," (encrypted.",[146,908,909],{"class":201},"length",[146,911,912],{"class":152}," \u003C",[146,914,915],{"class":156}," minLength) {\n",[146,917,919,922,924,926,928,931],{"class":148,"line":918},33,[146,920,921],{"class":152},"      throw",[146,923,727],{"class":152},[146,925,730],{"class":208},[146,927,212],{"class":156},[146,929,930],{"class":163},"'Invalid encrypted data format'",[146,932,559],{"class":156},[146,934,936],{"class":148,"line":935},34,[146,937,938],{"class":156},"    }\n",[146,940,942],{"class":148,"line":941},35,[146,943,189],{"emptyLinePlaceholder":188},[146,945,947,950,953,956,959],{"class":148,"line":946},36,[146,948,949],{"class":152},"    let",[146,951,952],{"class":156}," offset ",[146,954,955],{"class":152},"=",[146,957,958],{"class":201}," 0",[146,960,167],{"class":156},[146,962,964,966,968,970,973,976,979,982],{"class":148,"line":963},37,[146,965,540],{"class":152},[146,967,567],{"class":201},[146,969,205],{"class":152},[146,971,972],{"class":156}," encrypted.",[146,974,975],{"class":208},"subarray",[146,977,978],{"class":156},"(offset, offset ",[146,980,981],{"class":152},"+=",[146,983,984],{"class":156}," saltLen);\n",[146,986,988,990,992,994,996,998,1000,1002],{"class":148,"line":987},38,[146,989,540],{"class":152},[146,991,543],{"class":201},[146,993,205],{"class":152},[146,995,972],{"class":156},[146,997,975],{"class":208},[146,999,978],{"class":156},[146,1001,981],{"class":152},[146,1003,1004],{"class":156}," ivLen);\n",[146,1006,1008,1010,1012,1014,1016,1018,1020,1022],{"class":148,"line":1007},39,[146,1009,540],{"class":152},[146,1011,677],{"class":201},[146,1013,205],{"class":152},[146,1015,972],{"class":156},[146,1017,975],{"class":208},[146,1019,978],{"class":156},[146,1021,981],{"class":152},[146,1023,1024],{"class":156}," tagLen);\n",[146,1026,1028,1030,1033,1035,1037,1039],{"class":148,"line":1027},40,[146,1029,540],{"class":152},[146,1031,1032],{"class":201}," ciphertext",[146,1034,205],{"class":152},[146,1036,972],{"class":156},[146,1038,975],{"class":208},[146,1040,1041],{"class":156},"(offset);\n",[146,1043,1045],{"class":148,"line":1044},41,[146,1046,189],{"emptyLinePlaceholder":188},[146,1048,1050,1052,1054,1056,1058,1060,1062,1064,1066,1068],{"class":148,"line":1049},42,[146,1051,540],{"class":152},[146,1053,589],{"class":201},[146,1055,205],{"class":152},[146,1057,548],{"class":156},[146,1059,596],{"class":208},[146,1061,599],{"class":156},[146,1063,578],{"class":201},[146,1065,604],{"class":156},[146,1067,578],{"class":201},[146,1069,609],{"class":156},[146,1071,1073,1075,1078,1080,1082,1085,1087,1089],{"class":148,"line":1072},43,[146,1074,540],{"class":152},[146,1076,1077],{"class":201}," decipher",[146,1079,205],{"class":152},[146,1081,548],{"class":156},[146,1083,1084],{"class":208},"createDecipheriv",[146,1086,212],{"class":156},[146,1088,635],{"class":163},[146,1090,638],{"class":156},[146,1092,1094,1097,1100],{"class":148,"line":1093},44,[146,1095,1096],{"class":156},"    decipher.",[146,1098,1099],{"class":208},"setAuthTag",[146,1101,1102],{"class":156},"(tag);\n",[146,1104,1106],{"class":148,"line":1105},45,[146,1107,189],{"emptyLinePlaceholder":188},[146,1109,1111,1113,1115,1117,1120,1122],{"class":148,"line":1110},46,[146,1112,699],{"class":152},[146,1114,727],{"class":152},[146,1116,504],{"class":208},[146,1118,1119],{"class":156},"(Buffer.",[146,1121,654],{"class":208},[146,1123,1124],{"class":156},"([\n",[146,1126,1128,1131,1133],{"class":148,"line":1127},47,[146,1129,1130],{"class":156},"      decipher.",[146,1132,660],{"class":208},[146,1134,1135],{"class":156},"(ciphertext),\n",[146,1137,1139,1141,1143],{"class":148,"line":1138},48,[146,1140,1130],{"class":156},[146,1142,666],{"class":208},[146,1144,1145],{"class":156},"()\n",[146,1147,1149],{"class":148,"line":1148},49,[146,1150,1151],{"class":156},"    ]));\n",[146,1153,1155,1157,1159,1161,1164,1166,1169],{"class":148,"line":1154},50,[146,1156,712],{"class":156},[146,1158,715],{"class":152},[146,1160,494],{"class":156},[146,1162,1163],{"class":497},"error",[146,1165,501],{"class":152},[146,1167,1168],{"class":201}," unknown",[146,1170,1171],{"class":156},") {\n",[146,1173,1175,1177,1180,1182,1185,1188,1190,1193,1195,1197,1200],{"class":148,"line":1174},51,[146,1176,540],{"class":152},[146,1178,1179],{"class":201}," err",[146,1181,205],{"class":152},[146,1183,1184],{"class":156}," error ",[146,1186,1187],{"class":152},"instanceof",[146,1189,730],{"class":208},[146,1191,1192],{"class":152}," ?",[146,1194,1184],{"class":156},[146,1196,501],{"class":152},[146,1198,1199],{"class":201}," null",[146,1201,167],{"class":156},[146,1203,1205,1207,1210,1212,1215,1218,1221,1223,1226],{"class":148,"line":1204},52,[146,1206,540],{"class":152},[146,1208,1209],{"class":201}," code",[146,1211,205],{"class":152},[146,1213,1214],{"class":156}," (error ",[146,1216,1217],{"class":152},"as",[146,1219,1220],{"class":208}," NodeJS",[146,1222,33],{"class":156},[146,1224,1225],{"class":208},"ErrnoException",[146,1227,1228],{"class":156},").code;\n",[146,1230,1232,1234,1237,1240,1243,1246,1249,1252,1254,1257,1259,1262],{"class":148,"line":1231},53,[146,1233,903],{"class":152},[146,1235,1236],{"class":156}," (code ",[146,1238,1239],{"class":152},"===",[146,1241,1242],{"class":163}," 'ERR_CRYPTO_OPERATION_FAILED'",[146,1244,1245],{"class":152}," ||",[146,1247,1248],{"class":156}," err?.message.",[146,1250,1251],{"class":208},"toLowerCase",[146,1253,338],{"class":156},[146,1255,1256],{"class":208},"includes",[146,1258,212],{"class":156},[146,1260,1261],{"class":163},"'unsupported state'",[146,1263,1264],{"class":156},")) {\n",[146,1266,1268,1270,1272,1274,1276,1279],{"class":148,"line":1267},54,[146,1269,921],{"class":152},[146,1271,727],{"class":152},[146,1273,730],{"class":208},[146,1275,212],{"class":156},[146,1277,1278],{"class":163},"'Incorrect password or data'",[146,1280,559],{"class":156},[146,1282,1284],{"class":148,"line":1283},55,[146,1285,938],{"class":156},[146,1287,1289,1292,1295,1297,1300],{"class":148,"line":1288},56,[146,1290,1291],{"class":156},"    consola.",[146,1293,1294],{"class":208},"log",[146,1296,212],{"class":156},[146,1298,1299],{"class":163},"`Decryption failure`",[146,1301,559],{"class":156},[146,1303,1305,1307],{"class":148,"line":1304},57,[146,1306,724],{"class":152},[146,1308,1309],{"class":156}," error;\n",[146,1311,1313],{"class":148,"line":1312},58,[146,1314,761],{"class":156},[146,1316,1318],{"class":148,"line":1317},59,[146,1319,472],{"class":156},[10,1321,1322,1323,1326,1327,1330,1331,1336],{},"Now every time a new sub-wallet is created, or a main wallet is stored or updated I call ",[143,1324,1325],{},"encryptPrivateKey"," when I transfer funds, make transactions, etc, I call ",[143,1328,1329],{},"decryptPrivateKey",", perfect. This pair can probably be fed also to my ",[21,1332,1335],{"href":1333,"rel":1334},"https://github.com/Sergo706/utils",[25],"utils"," package later for some other weird use case I guess.",[10,1338,1339],{},"However the most notable problems with the above approach are:",[383,1341,1342,1345],{},[386,1343,1344],{},"There is no recovery mechanism, if you lose your password, you lose access to your wallets.",[386,1346,1347],{},"You need to provide your password every time you make sensitive actions, which can be insecure in cli contexts (AI tools, background processes, etc.).",[10,1349,1350],{},"But that is still more than enough for its purpose.",[10,1352,1353,1354,1357,1358,1365],{},"Then the code for creating wallets becomes really simple you just call ",[143,1355,1356],{},"Keypair.generate()"," method from the ",[21,1359,1362],{"href":1360,"rel":1361},"https://github.com/solana-foundation/solana-web3.js",[25],[143,1363,1364],{},"@solana/web3.js"," package:",[135,1367,1370],{"className":137,"code":1368,"filename":1369,"language":140,"meta":141,"style":141},"export async function createWallet(password: string) {\n    const newWallet = Keypair.generate();\n    const encP = encryptPrivateKey(newWallet.secretKey, password);\n\n   await db.insert(walletTable).values({\n        publicAddress: newWallet.publicKey.toBase58(),\n        privateKeyEncrypted: encP,\n        isMainWallet: false\n    });\n\n    consola.success('New Wallet created successfully', newWallet.publicKey);\n    return;\n}\n","createWallet.ts",[143,1371,1372,1395,1412,1426,1430,1450,1460,1465,1473,1478,1482,1497,1503],{"__ignoreMap":141},[146,1373,1374,1376,1379,1382,1385,1387,1389,1391,1393],{"class":148,"line":149},[146,1375,195],{"class":152},[146,1377,1378],{"class":152}," async",[146,1380,1381],{"class":152}," function",[146,1383,1384],{"class":208}," createWallet",[146,1386,212],{"class":156},[146,1388,510],{"class":497},[146,1390,501],{"class":152},[146,1392,515],{"class":201},[146,1394,1171],{"class":156},[146,1396,1397,1399,1402,1404,1407,1410],{"class":148,"line":170},[146,1398,540],{"class":152},[146,1400,1401],{"class":201}," newWallet",[146,1403,205],{"class":152},[146,1405,1406],{"class":156}," Keypair.",[146,1408,1409],{"class":208},"generate",[146,1411,688],{"class":156},[146,1413,1414,1416,1419,1421,1423],{"class":148,"line":185},[146,1415,540],{"class":152},[146,1417,1418],{"class":201}," encP",[146,1420,205],{"class":152},[146,1422,489],{"class":208},[146,1424,1425],{"class":156},"(newWallet.secretKey, password);\n",[146,1427,1428],{"class":148,"line":192},[146,1429,189],{"emptyLinePlaceholder":188},[146,1431,1432,1435,1438,1441,1444,1447],{"class":148,"line":221},[146,1433,1434],{"class":152},"   await",[146,1436,1437],{"class":156}," db.",[146,1439,1440],{"class":208},"insert",[146,1442,1443],{"class":156},"(walletTable).",[146,1445,1446],{"class":208},"values",[146,1448,1449],{"class":156},"({\n",[146,1451,1452,1455,1458],{"class":148,"line":256},[146,1453,1454],{"class":156},"        publicAddress: newWallet.publicKey.",[146,1456,1457],{"class":208},"toBase58",[146,1459,276],{"class":156},[146,1461,1462],{"class":148,"line":279},[146,1463,1464],{"class":156},"        privateKeyEncrypted: encP,\n",[146,1466,1467,1470],{"class":148,"line":300},[146,1468,1469],{"class":156},"        isMainWallet: ",[146,1471,1472],{"class":201},"false\n",[146,1474,1475],{"class":148,"line":321},[146,1476,1477],{"class":156},"    });\n",[146,1479,1480],{"class":148,"line":355},[146,1481,189],{"emptyLinePlaceholder":188},[146,1483,1484,1486,1489,1491,1494],{"class":148,"line":562},[146,1485,1291],{"class":156},[146,1487,1488],{"class":208},"success",[146,1490,212],{"class":156},[146,1492,1493],{"class":163},"'New Wallet created successfully'",[146,1495,1496],{"class":156},", newWallet.publicKey);\n",[146,1498,1499,1501],{"class":148,"line":584},[146,1500,699],{"class":152},[146,1502,167],{"class":156},[146,1504,1505],{"class":148,"line":612},[146,1506,1507],{"class":156},"}\n",[10,1509,1510,1511,1514,1515,1518],{},"For storing the main wallet, I thought to let a user provide a file path, since its a cli right now. And because the format of the key can come as a ",[143,1512,1513],{},"base58"," and ",[143,1516,1517],{},"Uint8Array"," the code should detect that, try to parse it, and forbid overriding an existing main key if one already exists:",[135,1520,1522],{"className":137,"code":1521,"filename":1369,"language":140,"meta":141,"style":141},"export async function storeMain(password: string, secretPath: string) {\n    const [main] = await db.select().from(walletTable).where(eq(walletTable.isMainWallet, true));\n\n    if (main?.privateKeyEncrypted || main?.publicAddress) {\n        consola.error(`MAIN wallet already exists!`);\n        throw new Error(`MAIN wallet already exists!`);\n    }\n    \n    const filePath = path.resolve(process.cwd(), secretPath);\n    const secretKeyString = fs.readFileSync(filePath, \"utf8\");\n\n    if (!secretKeyString) {\n        consola.error(`File path didn't found at ${secretPath}`);\n        throw new Error(`File path didn't found at ${secretPath}`);\n    }\n\n    try {\n        const trimmed = secretKeyString.trim();\n        let secretKeyBytes: Uint8Array;\n\n        try {\n            const parsed = JSON.parse(trimmed)  as number[] | string;\n            if (Array.isArray(parsed) && parsed.every((n) => typeof n === 'number')) {\n                secretKeyBytes = Uint8Array.from(parsed);\n\n            } else if (typeof parsed === 'string') {\n                secretKeyBytes = bs58.decode(parsed);\n            } else {\n                throw new Error('Unsupported JSON secret format');\n            }\n\n        } catch {\n            secretKeyBytes = bs58.decode(trimmed);\n        }\n\n        const pub = Keypair.fromSecretKey(secretKeyBytes);\n        const encP = encryptPrivateKey(pub.secretKey, password);\n\n        await db.insert(walletTable).values({\n            publicAddress: pub.publicKey.toBase58(),\n            privateKeyEncrypted: encP,\n            isMainWallet: true\n        });\n\n        consola.success('Main Wallet stored successfully', pub.publicKey.toBase58());\n        return;\n    } catch (err) {\n        consola.error(\"Invalid secret key format. Ensure it is a JSON array of numbers, a base58 string, base64, or hex.\");\n        consola.error(err);\n        throw new Error(\"Invalid secret key format. Ensure it is a JSON array of numbers, a base58 string, base64, or hex.\");\n    }\n}\n",[143,1523,1524,1554,1599,1603,1616,1630,1645,1649,1653,1677,1700,1704,1716,1733,1751,1755,1759,1766,1784,1798,1802,1809,1845,1893,1908,1912,1938,1952,1960,1976,1981,1985,1994,2008,2013,2017,2034,2047,2051,2066,2075,2080,2088,2093,2097,2116,2123,2132,2145,2154,2168,2172],{"__ignoreMap":141},[146,1525,1526,1528,1530,1532,1535,1537,1539,1541,1543,1545,1548,1550,1552],{"class":148,"line":149},[146,1527,195],{"class":152},[146,1529,1378],{"class":152},[146,1531,1381],{"class":152},[146,1533,1534],{"class":208}," storeMain",[146,1536,212],{"class":156},[146,1538,510],{"class":497},[146,1540,501],{"class":152},[146,1542,515],{"class":201},[146,1544,507],{"class":156},[146,1546,1547],{"class":497},"secretPath",[146,1549,501],{"class":152},[146,1551,515],{"class":201},[146,1553,1171],{"class":156},[146,1555,1556,1558,1561,1564,1567,1569,1572,1574,1577,1579,1581,1583,1586,1588,1591,1594,1596],{"class":148,"line":170},[146,1557,540],{"class":152},[146,1559,1560],{"class":156}," [",[146,1562,1563],{"class":201},"main",[146,1565,1566],{"class":156},"] ",[146,1568,955],{"class":152},[146,1570,1571],{"class":152}," await",[146,1573,1437],{"class":156},[146,1575,1576],{"class":208},"select",[146,1578,338],{"class":156},[146,1580,160],{"class":208},[146,1582,1443],{"class":156},[146,1584,1585],{"class":208},"where",[146,1587,212],{"class":156},[146,1589,1590],{"class":208},"eq",[146,1592,1593],{"class":156},"(walletTable.isMainWallet, ",[146,1595,250],{"class":201},[146,1597,1598],{"class":156},"));\n",[146,1600,1601],{"class":148,"line":185},[146,1602,189],{"emptyLinePlaceholder":188},[146,1604,1605,1607,1610,1613],{"class":148,"line":192},[146,1606,903],{"class":152},[146,1608,1609],{"class":156}," (main?.privateKeyEncrypted ",[146,1611,1612],{"class":152},"||",[146,1614,1615],{"class":156}," main?.publicAddress) {\n",[146,1617,1618,1621,1623,1625,1628],{"class":148,"line":221},[146,1619,1620],{"class":156},"        consola.",[146,1622,1163],{"class":208},[146,1624,212],{"class":156},[146,1626,1627],{"class":163},"`MAIN wallet already exists!`",[146,1629,559],{"class":156},[146,1631,1632,1635,1637,1639,1641,1643],{"class":148,"line":256},[146,1633,1634],{"class":152},"        throw",[146,1636,727],{"class":152},[146,1638,730],{"class":208},[146,1640,212],{"class":156},[146,1642,1627],{"class":163},[146,1644,559],{"class":156},[146,1646,1647],{"class":148,"line":279},[146,1648,938],{"class":156},[146,1650,1651],{"class":148,"line":300},[146,1652,615],{"class":156},[146,1654,1655,1657,1660,1662,1665,1668,1671,1674],{"class":148,"line":321},[146,1656,540],{"class":152},[146,1658,1659],{"class":201}," filePath",[146,1661,205],{"class":152},[146,1663,1664],{"class":156}," path.",[146,1666,1667],{"class":208},"resolve",[146,1669,1670],{"class":156},"(process.",[146,1672,1673],{"class":208},"cwd",[146,1675,1676],{"class":156},"(), secretPath);\n",[146,1678,1679,1681,1684,1686,1689,1692,1695,1698],{"class":148,"line":355},[146,1680,540],{"class":152},[146,1682,1683],{"class":201}," secretKeyString",[146,1685,205],{"class":152},[146,1687,1688],{"class":156}," fs.",[146,1690,1691],{"class":208},"readFileSync",[146,1693,1694],{"class":156},"(filePath, ",[146,1696,1697],{"class":163},"\"utf8\"",[146,1699,559],{"class":156},[146,1701,1702],{"class":148,"line":562},[146,1703,189],{"emptyLinePlaceholder":188},[146,1705,1706,1708,1710,1713],{"class":148,"line":584},[146,1707,903],{"class":152},[146,1709,494],{"class":156},[146,1711,1712],{"class":152},"!",[146,1714,1715],{"class":156},"secretKeyString) {\n",[146,1717,1718,1720,1722,1724,1727,1729,1731],{"class":148,"line":612},[146,1719,1620],{"class":156},[146,1721,1163],{"class":208},[146,1723,212],{"class":156},[146,1725,1726],{"class":163},"`File path didn't found at ${",[146,1728,1547],{"class":156},[146,1730,753],{"class":163},[146,1732,559],{"class":156},[146,1734,1735,1737,1739,1741,1743,1745,1747,1749],{"class":148,"line":618},[146,1736,1634],{"class":152},[146,1738,727],{"class":152},[146,1740,730],{"class":208},[146,1742,212],{"class":156},[146,1744,1726],{"class":163},[146,1746,1547],{"class":156},[146,1748,753],{"class":163},[146,1750,559],{"class":156},[146,1752,1753],{"class":148,"line":641},[146,1754,938],{"class":156},[146,1756,1757],{"class":148,"line":672},[146,1758,189],{"emptyLinePlaceholder":188},[146,1760,1761,1764],{"class":148,"line":691},[146,1762,1763],{"class":152},"    try",[146,1765,424],{"class":156},[146,1767,1768,1771,1774,1776,1779,1782],{"class":148,"line":696},[146,1769,1770],{"class":152},"        const",[146,1772,1773],{"class":201}," trimmed",[146,1775,205],{"class":152},[146,1777,1778],{"class":156}," secretKeyString.",[146,1780,1781],{"class":208},"trim",[146,1783,688],{"class":156},[146,1785,1786,1789,1792,1794,1796],{"class":148,"line":709},[146,1787,1788],{"class":152},"        let",[146,1790,1791],{"class":156}," secretKeyBytes",[146,1793,501],{"class":152},[146,1795,504],{"class":208},[146,1797,167],{"class":156},[146,1799,1800],{"class":148,"line":721},[146,1801,189],{"emptyLinePlaceholder":188},[146,1803,1804,1807],{"class":148,"line":758},[146,1805,1806],{"class":152},"        try",[146,1808,424],{"class":156},[146,1810,1811,1814,1817,1819,1822,1824,1827,1830,1832,1835,1838,1841,1843],{"class":148,"line":764},[146,1812,1813],{"class":152},"            const",[146,1815,1816],{"class":201}," parsed",[146,1818,205],{"class":152},[146,1820,1821],{"class":201}," JSON",[146,1823,33],{"class":156},[146,1825,1826],{"class":208},"parse",[146,1828,1829],{"class":156},"(trimmed)  ",[146,1831,1217],{"class":152},[146,1833,1834],{"class":201}," number",[146,1836,1837],{"class":156},"[] ",[146,1839,1840],{"class":152},"|",[146,1842,515],{"class":201},[146,1844,167],{"class":156},[146,1846,1847,1850,1853,1856,1859,1862,1865,1868,1871,1874,1877,1880,1883,1886,1888,1891],{"class":148,"line":769},[146,1848,1849],{"class":152},"            if",[146,1851,1852],{"class":156}," (Array.",[146,1854,1855],{"class":208},"isArray",[146,1857,1858],{"class":156},"(parsed) ",[146,1860,1861],{"class":152},"&&",[146,1863,1864],{"class":156}," parsed.",[146,1866,1867],{"class":208},"every",[146,1869,1870],{"class":156},"((",[146,1872,1873],{"class":497},"n",[146,1875,1876],{"class":156},") ",[146,1878,1879],{"class":152},"=>",[146,1881,1882],{"class":152}," typeof",[146,1884,1885],{"class":156}," n ",[146,1887,1239],{"class":152},[146,1889,1890],{"class":163}," 'number'",[146,1892,1264],{"class":156},[146,1894,1895,1898,1900,1903,1905],{"class":148,"line":774},[146,1896,1897],{"class":156},"                secretKeyBytes ",[146,1899,955],{"class":152},[146,1901,1902],{"class":156}," Uint8Array.",[146,1904,160],{"class":208},[146,1906,1907],{"class":156},"(parsed);\n",[146,1909,1910],{"class":148,"line":813},[146,1911,189],{"emptyLinePlaceholder":188},[146,1913,1914,1917,1920,1923,1925,1928,1931,1933,1936],{"class":148,"line":820},[146,1915,1916],{"class":156},"            } ",[146,1918,1919],{"class":152},"else",[146,1921,1922],{"class":152}," if",[146,1924,494],{"class":156},[146,1926,1927],{"class":152},"typeof",[146,1929,1930],{"class":156}," parsed ",[146,1932,1239],{"class":152},[146,1934,1935],{"class":163}," 'string'",[146,1937,1171],{"class":156},[146,1939,1940,1942,1944,1947,1950],{"class":148,"line":825},[146,1941,1897],{"class":156},[146,1943,955],{"class":152},[146,1945,1946],{"class":156}," bs58.",[146,1948,1949],{"class":208},"decode",[146,1951,1907],{"class":156},[146,1953,1954,1956,1958],{"class":148,"line":840},[146,1955,1916],{"class":156},[146,1957,1919],{"class":152},[146,1959,424],{"class":156},[146,1961,1962,1965,1967,1969,1971,1974],{"class":148,"line":856},[146,1963,1964],{"class":152},"                throw",[146,1966,727],{"class":152},[146,1968,730],{"class":208},[146,1970,212],{"class":156},[146,1972,1973],{"class":163},"'Unsupported JSON secret format'",[146,1975,559],{"class":156},[146,1977,1978],{"class":148,"line":871},[146,1979,1980],{"class":156},"            }\n",[146,1982,1983],{"class":148,"line":895},[146,1984,189],{"emptyLinePlaceholder":188},[146,1986,1987,1990,1992],{"class":148,"line":900},[146,1988,1989],{"class":156},"        } ",[146,1991,715],{"class":152},[146,1993,424],{"class":156},[146,1995,1996,1999,2001,2003,2005],{"class":148,"line":918},[146,1997,1998],{"class":156},"            secretKeyBytes ",[146,2000,955],{"class":152},[146,2002,1946],{"class":156},[146,2004,1949],{"class":208},[146,2006,2007],{"class":156},"(trimmed);\n",[146,2009,2010],{"class":148,"line":935},[146,2011,2012],{"class":156},"        }\n",[146,2014,2015],{"class":148,"line":941},[146,2016,189],{"emptyLinePlaceholder":188},[146,2018,2019,2021,2024,2026,2028,2031],{"class":148,"line":946},[146,2020,1770],{"class":152},[146,2022,2023],{"class":201}," pub",[146,2025,205],{"class":152},[146,2027,1406],{"class":156},[146,2029,2030],{"class":208},"fromSecretKey",[146,2032,2033],{"class":156},"(secretKeyBytes);\n",[146,2035,2036,2038,2040,2042,2044],{"class":148,"line":963},[146,2037,1770],{"class":152},[146,2039,1418],{"class":201},[146,2041,205],{"class":152},[146,2043,489],{"class":208},[146,2045,2046],{"class":156},"(pub.secretKey, password);\n",[146,2048,2049],{"class":148,"line":987},[146,2050,189],{"emptyLinePlaceholder":188},[146,2052,2053,2056,2058,2060,2062,2064],{"class":148,"line":1007},[146,2054,2055],{"class":152},"        await",[146,2057,1437],{"class":156},[146,2059,1440],{"class":208},[146,2061,1443],{"class":156},[146,2063,1446],{"class":208},[146,2065,1449],{"class":156},[146,2067,2068,2071,2073],{"class":148,"line":1027},[146,2069,2070],{"class":156},"            publicAddress: pub.publicKey.",[146,2072,1457],{"class":208},[146,2074,276],{"class":156},[146,2076,2077],{"class":148,"line":1044},[146,2078,2079],{"class":156},"            privateKeyEncrypted: encP,\n",[146,2081,2082,2085],{"class":148,"line":1049},[146,2083,2084],{"class":156},"            isMainWallet: ",[146,2086,2087],{"class":201},"true\n",[146,2089,2090],{"class":148,"line":1072},[146,2091,2092],{"class":156},"        });\n",[146,2094,2095],{"class":148,"line":1093},[146,2096,189],{"emptyLinePlaceholder":188},[146,2098,2099,2101,2103,2105,2108,2111,2113],{"class":148,"line":1105},[146,2100,1620],{"class":156},[146,2102,1488],{"class":208},[146,2104,212],{"class":156},[146,2106,2107],{"class":163},"'Main Wallet stored successfully'",[146,2109,2110],{"class":156},", pub.publicKey.",[146,2112,1457],{"class":208},[146,2114,2115],{"class":156},"());\n",[146,2117,2118,2121],{"class":148,"line":1110},[146,2119,2120],{"class":152},"        return",[146,2122,167],{"class":156},[146,2124,2125,2128,2130],{"class":148,"line":1127},[146,2126,2127],{"class":156},"    } ",[146,2129,715],{"class":152},[146,2131,718],{"class":156},[146,2133,2134,2136,2138,2140,2143],{"class":148,"line":1138},[146,2135,1620],{"class":156},[146,2137,1163],{"class":208},[146,2139,212],{"class":156},[146,2141,2142],{"class":163},"\"Invalid secret key format. Ensure it is a JSON array of numbers, a base58 string, base64, or hex.\"",[146,2144,559],{"class":156},[146,2146,2147,2149,2151],{"class":148,"line":1148},[146,2148,1620],{"class":156},[146,2150,1163],{"class":208},[146,2152,2153],{"class":156},"(err);\n",[146,2155,2156,2158,2160,2162,2164,2166],{"class":148,"line":1154},[146,2157,1634],{"class":152},[146,2159,727],{"class":152},[146,2161,730],{"class":208},[146,2163,212],{"class":156},[146,2165,2142],{"class":163},[146,2167,559],{"class":156},[146,2169,2170],{"class":148,"line":1174},[146,2171,938],{"class":156},[146,2173,2174],{"class":148,"line":1204},[146,2175,1507],{"class":156},[10,2177,2178],{},"At this point I got the foundation of the bots set up.",[383,2180,2181,2184,2187,2190],{},[386,2182,2183],{},"Only one main wallet can exist at a time",[386,2185,2186],{},"Sub wallets can be created as much as you want (eg the bots)",[386,2188,2189],{},"Everything is encrypted and decrypted on demand",[386,2191,2192],{},"Everything except the user password is stored in sqlite",[10,2194,2195],{},"I got up and continued with the wallets logic:",[383,2197,2198,2204,2210,2216],{},[386,2199,2200,2203],{},[143,2201,2202],{},"drainWalletsToMain"," to drain all sub-wallets funds back to the main wallet, that accepts the user password for decryption, calculates the solana fees and skips wallets that don't have enough lamports (or SOL. 1 SOL === 1,000,000,000).",[386,2205,2206,2209],{},[143,2207,2208],{},"distributeGas"," to split SOL to sub-wallets that accepts also the password for decryption, and the amount of the SOL to split to each wallet. This one calculates the total fees for all transactions that will be performed + the specified amount if there is not enough SOL it throws.",[386,2211,2212,2215],{},[143,2213,2214],{},"getByPubkey"," To read metadata by a specific public key of a wallet such as SOL in, creation date etc.",[386,2217,2218,2221,2222,2224],{},[143,2219,2220],{},"getAllWalletsView"," Same as ",[143,2223,2214],{}," but to get the view of all wallets, including the main one.",[10,2226,2227],{},"Both also accept the password.",[375,2229,2231],{"id":2230},"setting-up-the-trade-logic","Setting up the Trade Logic",[10,2233,2234],{},"With the wallet functions wired, I first focused heavily on the core features:",[383,2236,2237,2240,2243],{},[386,2238,2239],{},"Buy: To start a mass buy using the sub-wallets with configurable wallet amounts to use, SOL amounts to use from each wallet, and the target mint.",[386,2241,2242],{},"Sell: To start a mass sell, also with the number of wallets to use, the target mint, and the percentage to sell from each wallet.",[386,2244,2245],{},"Out: To panic out from all positions.",[10,2247,2248],{},"All operations also accept the password to decrypt the wallets that are going to be used.",[10,2250,2251],{},"Because the CLI runs entirely locally, and needs to manage user specific apis, I made another table:",[135,2253,2256],{"className":137,"code":2254,"filename":2255,"language":140,"meta":141,"style":141},"import { blob, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { sql } from \"drizzle-orm\";\n\nexport const apiAndRpcTable = sqliteTable('api_rpc', {\n    id: integer('id', {mode: 'number'}).primaryKey({ autoIncrement: true }),\n    createdAt: integer('created_at').notNull().default(sql`(cast(unixepoch() as int))`),\n    encryptedApiKey: blob('private_key_encrypted').notNull(),\n    rpc: text('rpc', {mode: 'text' }).notNull(),\n});\n","secrets.ts",[143,2257,2258,2270,2282,2286,2306,2330,2356,2373,2397],{"__ignoreMap":141},[146,2259,2260,2262,2264,2266,2268],{"class":148,"line":149},[146,2261,153],{"class":152},[146,2263,157],{"class":156},[146,2265,160],{"class":152},[146,2267,164],{"class":163},[146,2269,167],{"class":156},[146,2271,2272,2274,2276,2278,2280],{"class":148,"line":170},[146,2273,153],{"class":152},[146,2275,175],{"class":156},[146,2277,160],{"class":152},[146,2279,180],{"class":163},[146,2281,167],{"class":156},[146,2283,2284],{"class":148,"line":185},[146,2285,189],{"emptyLinePlaceholder":188},[146,2287,2288,2290,2292,2295,2297,2299,2301,2304],{"class":148,"line":192},[146,2289,195],{"class":152},[146,2291,198],{"class":152},[146,2293,2294],{"class":201}," apiAndRpcTable",[146,2296,205],{"class":152},[146,2298,209],{"class":208},[146,2300,212],{"class":156},[146,2302,2303],{"class":163},"'api_rpc'",[146,2305,218],{"class":156},[146,2307,2308,2310,2312,2314,2316,2318,2320,2322,2324,2326,2328],{"class":148,"line":221},[146,2309,224],{"class":156},[146,2311,227],{"class":208},[146,2313,212],{"class":156},[146,2315,232],{"class":163},[146,2317,235],{"class":156},[146,2319,238],{"class":163},[146,2321,241],{"class":156},[146,2323,244],{"class":208},[146,2325,247],{"class":156},[146,2327,250],{"class":201},[146,2329,253],{"class":156},[146,2331,2332,2334,2336,2338,2340,2342,2344,2346,2348,2350,2352,2354],{"class":148,"line":256},[146,2333,324],{"class":156},[146,2335,227],{"class":208},[146,2337,212],{"class":156},[146,2339,331],{"class":163},[146,2341,270],{"class":156},[146,2343,316],{"class":208},[146,2345,338],{"class":156},[146,2347,341],{"class":208},[146,2349,212],{"class":156},[146,2351,346],{"class":208},[146,2353,349],{"class":163},[146,2355,352],{"class":156},[146,2357,2358,2361,2363,2365,2367,2369,2371],{"class":148,"line":279},[146,2359,2360],{"class":156},"    encryptedApiKey: ",[146,2362,306],{"class":208},[146,2364,212],{"class":156},[146,2366,311],{"class":163},[146,2368,270],{"class":156},[146,2370,316],{"class":208},[146,2372,276],{"class":156},[146,2374,2375,2378,2380,2382,2385,2387,2390,2393,2395],{"class":148,"line":300},[146,2376,2377],{"class":156},"    rpc: ",[146,2379,262],{"class":208},[146,2381,212],{"class":156},[146,2383,2384],{"class":163},"'rpc'",[146,2386,235],{"class":156},[146,2388,2389],{"class":163},"'text'",[146,2391,2392],{"class":156}," }).",[146,2394,316],{"class":208},[146,2396,276],{"class":156},[146,2398,2399],{"class":148,"line":321},[146,2400,358],{"class":156},[10,2402,2403,2404,2407],{},"The ",[143,2405,2406],{},"encryptedApiKey"," field is the Jupiter API key, which is also encrypted using the mechanism described above, and decrypted with the password inside the trade operations.",[10,2409,2403,2410,2413],{},[143,2411,2412],{},"rpc"," field is the URL of the RPC provider being used; this field is not encrypted.",[10,2415,2416,2417,2422],{},"While I am not going to paste all the code for these functions in this post (you can find it ",[21,2418,2421],{"href":2419,"rel":2420},"https://github.com/Sergo706/solana-bots/tree/main/src/lib/transactions",[25],"here","), the main challenge I found difficult to solve was rate limits.",[10,2424,2425],{},"The solutions were either to use a paid provider or to break down the logic to be rarely rate-limited by the public RPC.",[10,2427,2428],{},"You already guessed it, I chose the second option. To stop the bot from spamming the RPC and Jupiter API all at once, I wrote a custom fetch wrapper. It catches 429 errors and applies an exponential backoff with a bit of random jitter before it retries.",[135,2430,2433],{"className":137,"code":2431,"filename":2432,"language":140,"meta":141,"style":141},"import consola from \"consola\";\n\nexport async function fetchWithRetry(url: string, retries = 5, delay = 1000, init?: RequestInit) {\n  try {\n    const res = await fetch(url, init);\n    if (res.status === 429 && retries > 0) {\n\n      const jitter = Math.random() * (delay * 0.5);\n      const totalWait = delay + jitter;\n\n      consola.warn(`[429] Rate limited on ${url}. Retrying in ${(totalWait / 1000).toFixed(2)}s...`);\n      await new Promise(res => setTimeout(res, totalWait));\n      return await fetchWithRetry(url, retries - 1, delay * 2, init);\n    }\n    \n    return res;\n  } catch (err) {\n    if (retries > 0) {\n      consola.error(`[Network Error] ${url}. Retrying...`);\n      return fetchWithRetry(url, retries - 1, delay * 2, init);\n    }\n    throw err;\n  }\n}\n","fetchWithRetry.ts",[143,2434,2435,2449,2453,2506,2512,2529,2554,2558,2590,2607,2611,2656,2679,2708,2712,2716,2723,2731,2744,2762,2782,2786,2793,2797],{"__ignoreMap":141},[146,2436,2437,2439,2442,2444,2447],{"class":148,"line":149},[146,2438,153],{"class":152},[146,2440,2441],{"class":156}," consola ",[146,2443,160],{"class":152},[146,2445,2446],{"class":163}," \"consola\"",[146,2448,167],{"class":156},[146,2450,2451],{"class":148,"line":170},[146,2452,189],{"emptyLinePlaceholder":188},[146,2454,2455,2457,2459,2461,2464,2466,2469,2471,2473,2475,2478,2480,2483,2485,2488,2490,2493,2495,2498,2501,2504],{"class":148,"line":185},[146,2456,195],{"class":152},[146,2458,1378],{"class":152},[146,2460,1381],{"class":152},[146,2462,2463],{"class":208}," fetchWithRetry",[146,2465,212],{"class":156},[146,2467,2468],{"class":497},"url",[146,2470,501],{"class":152},[146,2472,515],{"class":201},[146,2474,507],{"class":156},[146,2476,2477],{"class":497},"retries",[146,2479,205],{"class":152},[146,2481,2482],{"class":201}," 5",[146,2484,507],{"class":156},[146,2486,2487],{"class":497},"delay",[146,2489,205],{"class":152},[146,2491,2492],{"class":201}," 1000",[146,2494,507],{"class":156},[146,2496,2497],{"class":497},"init",[146,2499,2500],{"class":152},"?:",[146,2502,2503],{"class":208}," RequestInit",[146,2505,1171],{"class":156},[146,2507,2508,2510],{"class":148,"line":192},[146,2509,533],{"class":152},[146,2511,424],{"class":156},[146,2513,2514,2516,2519,2521,2523,2526],{"class":148,"line":221},[146,2515,540],{"class":152},[146,2517,2518],{"class":201}," res",[146,2520,205],{"class":152},[146,2522,1571],{"class":152},[146,2524,2525],{"class":208}," fetch",[146,2527,2528],{"class":156},"(url, init);\n",[146,2530,2531,2533,2536,2538,2541,2544,2547,2550,2552],{"class":148,"line":256},[146,2532,903],{"class":152},[146,2534,2535],{"class":156}," (res.status ",[146,2537,1239],{"class":152},[146,2539,2540],{"class":201}," 429",[146,2542,2543],{"class":152}," &&",[146,2545,2546],{"class":156}," retries ",[146,2548,2549],{"class":152},">",[146,2551,958],{"class":201},[146,2553,1171],{"class":156},[146,2555,2556],{"class":148,"line":279},[146,2557,189],{"emptyLinePlaceholder":188},[146,2559,2560,2563,2566,2568,2571,2574,2577,2580,2583,2585,2588],{"class":148,"line":300},[146,2561,2562],{"class":152},"      const",[146,2564,2565],{"class":201}," jitter",[146,2567,205],{"class":152},[146,2569,2570],{"class":156}," Math.",[146,2572,2573],{"class":208},"random",[146,2575,2576],{"class":156},"() ",[146,2578,2579],{"class":152},"*",[146,2581,2582],{"class":156}," (delay ",[146,2584,2579],{"class":152},[146,2586,2587],{"class":201}," 0.5",[146,2589,559],{"class":156},[146,2591,2592,2594,2597,2599,2602,2604],{"class":148,"line":321},[146,2593,2562],{"class":152},[146,2595,2596],{"class":201}," totalWait",[146,2598,205],{"class":152},[146,2600,2601],{"class":156}," delay ",[146,2603,884],{"class":152},[146,2605,2606],{"class":156}," jitter;\n",[146,2608,2609],{"class":148,"line":355},[146,2610,189],{"emptyLinePlaceholder":188},[146,2612,2613,2616,2619,2621,2624,2626,2629,2631,2634,2637,2639,2641,2644,2646,2649,2651,2654],{"class":148,"line":562},[146,2614,2615],{"class":156},"      consola.",[146,2617,2618],{"class":208},"warn",[146,2620,212],{"class":156},[146,2622,2623],{"class":163},"`[429] Rate limited on ${",[146,2625,2468],{"class":156},[146,2627,2628],{"class":163},"}. Retrying in ${",[146,2630,212],{"class":163},[146,2632,2633],{"class":156},"totalWait",[146,2635,2636],{"class":152}," /",[146,2638,2492],{"class":201},[146,2640,270],{"class":163},[146,2642,2643],{"class":208},"toFixed",[146,2645,212],{"class":163},[146,2647,2648],{"class":201},"2",[146,2650,518],{"class":163},[146,2652,2653],{"class":163},"}s...`",[146,2655,559],{"class":156},[146,2657,2658,2661,2663,2666,2668,2671,2673,2676],{"class":148,"line":584},[146,2659,2660],{"class":152},"      await",[146,2662,727],{"class":152},[146,2664,2665],{"class":201}," Promise",[146,2667,212],{"class":156},[146,2669,2670],{"class":497},"res",[146,2672,526],{"class":152},[146,2674,2675],{"class":208}," setTimeout",[146,2677,2678],{"class":156},"(res, totalWait));\n",[146,2680,2681,2684,2686,2688,2691,2694,2697,2700,2702,2705],{"class":148,"line":612},[146,2682,2683],{"class":152},"      return",[146,2685,1571],{"class":152},[146,2687,2463],{"class":208},[146,2689,2690],{"class":156},"(url, retries ",[146,2692,2693],{"class":152},"-",[146,2695,2696],{"class":201}," 1",[146,2698,2699],{"class":156},", delay ",[146,2701,2579],{"class":152},[146,2703,2704],{"class":201}," 2",[146,2706,2707],{"class":156},", init);\n",[146,2709,2710],{"class":148,"line":618},[146,2711,938],{"class":156},[146,2713,2714],{"class":148,"line":641},[146,2715,615],{"class":156},[146,2717,2718,2720],{"class":148,"line":672},[146,2719,699],{"class":152},[146,2721,2722],{"class":156}," res;\n",[146,2724,2725,2727,2729],{"class":148,"line":691},[146,2726,712],{"class":156},[146,2728,715],{"class":152},[146,2730,718],{"class":156},[146,2732,2733,2735,2738,2740,2742],{"class":148,"line":696},[146,2734,903],{"class":152},[146,2736,2737],{"class":156}," (retries ",[146,2739,2549],{"class":152},[146,2741,958],{"class":201},[146,2743,1171],{"class":156},[146,2745,2746,2748,2750,2752,2755,2757,2760],{"class":148,"line":709},[146,2747,2615],{"class":156},[146,2749,1163],{"class":208},[146,2751,212],{"class":156},[146,2753,2754],{"class":163},"`[Network Error] ${",[146,2756,2468],{"class":156},[146,2758,2759],{"class":163},"}. Retrying...`",[146,2761,559],{"class":156},[146,2763,2764,2766,2768,2770,2772,2774,2776,2778,2780],{"class":148,"line":721},[146,2765,2683],{"class":152},[146,2767,2463],{"class":208},[146,2769,2690],{"class":156},[146,2771,2693],{"class":152},[146,2773,2696],{"class":201},[146,2775,2699],{"class":156},[146,2777,2579],{"class":152},[146,2779,2704],{"class":201},[146,2781,2707],{"class":156},[146,2783,2784],{"class":148,"line":758},[146,2785,938],{"class":156},[146,2787,2788,2790],{"class":148,"line":764},[146,2789,724],{"class":152},[146,2791,2792],{"class":156}," err;\n",[146,2794,2795],{"class":148,"line":769},[146,2796,761],{"class":156},[146,2798,2799],{"class":148,"line":774},[146,2800,1507],{"class":156},[10,2802,2803,2804,2807],{},"Next, in the mass operation functions themselves (like massBuy, massSell, and massOut), instead of executing every wallet transaction simultaneously with a giant ",[143,2805,2806],{},"Promise.all",", I chunked the processes. By grouping the wallets into batches and adding a sleep delay between them, it gives the public API time to breathe.",[10,2809,2810,2811,501],{},"Here is a snippet of how that processing loop looks inside ",[143,2812,2813],{},"massBuy.ts",[135,2815,2817],{"className":137,"code":2816,"language":140,"meta":141,"style":141},"await chunkProcess(walletData, chunksToProcess, async (chunk) => {\n        await Promise.all(\n            chunk.map(async ({balance, wallet}) => {\n\n                // balance checks and URL setup \n\n                const orderResponse = await fetchWithRetry(String(orderUrl), 5, 500, { \n                    headers: { \n                        \"x-api-key\": API_KEY \n                    } \n                });\n\n                // transaction signing \n\n              const executeResponse = await fetchWithRetry(`${JUPITER_BASE_URL}/execute`, 5, 500, {\n                    method: \"POST\",\n                    headers: {\n                        \"Content-Type\": \"application/json\",\n                        \"x-api-key\": API_KEY,\n                    },\n                    body: JSON.stringify({\n                        signedTransaction: Buffer.from(transaction.serialize()).toString(\"base64\"),\n                        requestId: orderResult.requestId,\n                    }),\n                });\n\n            })\n        );\n         // Sleep after each chunk to avoid hitting rate limits on the next batch\n         await sleep(1000);\n    });\n\n",[143,2818,2819,2844,2858,2888,2892,2898,2902,2935,2940,2954,2959,2964,2968,2973,2977,3012,3022,3027,3039,3049,3054,3069,3095,3100,3105,3109,3113,3118,3123,3128,3143],{"__ignoreMap":141},[146,2820,2821,2824,2827,2830,2833,2835,2838,2840,2842],{"class":148,"line":149},[146,2822,2823],{"class":152},"await",[146,2825,2826],{"class":208}," chunkProcess",[146,2828,2829],{"class":156},"(walletData, chunksToProcess, ",[146,2831,2832],{"class":152},"async",[146,2834,494],{"class":156},[146,2836,2837],{"class":497},"chunk",[146,2839,1876],{"class":156},[146,2841,1879],{"class":152},[146,2843,424],{"class":156},[146,2845,2846,2848,2850,2852,2855],{"class":148,"line":170},[146,2847,2055],{"class":152},[146,2849,2665],{"class":201},[146,2851,33],{"class":156},[146,2853,2854],{"class":208},"all",[146,2856,2857],{"class":156},"(\n",[146,2859,2860,2863,2866,2868,2870,2873,2876,2878,2881,2884,2886],{"class":148,"line":185},[146,2861,2862],{"class":156},"            chunk.",[146,2864,2865],{"class":208},"map",[146,2867,212],{"class":156},[146,2869,2832],{"class":152},[146,2871,2872],{"class":156}," ({",[146,2874,2875],{"class":497},"balance",[146,2877,507],{"class":156},[146,2879,2880],{"class":497},"wallet",[146,2882,2883],{"class":156},"}) ",[146,2885,1879],{"class":152},[146,2887,424],{"class":156},[146,2889,2890],{"class":148,"line":192},[146,2891,189],{"emptyLinePlaceholder":188},[146,2893,2894],{"class":148,"line":221},[146,2895,2897],{"class":2896},"sCsY4","                // balance checks and URL setup \n",[146,2899,2900],{"class":148,"line":256},[146,2901,189],{"emptyLinePlaceholder":188},[146,2903,2904,2907,2910,2912,2914,2916,2918,2921,2924,2927,2929,2932],{"class":148,"line":279},[146,2905,2906],{"class":152},"                const",[146,2908,2909],{"class":201}," orderResponse",[146,2911,205],{"class":152},[146,2913,1571],{"class":152},[146,2915,2463],{"class":208},[146,2917,212],{"class":156},[146,2919,2920],{"class":208},"String",[146,2922,2923],{"class":156},"(orderUrl), ",[146,2925,2926],{"class":201},"5",[146,2928,507],{"class":156},[146,2930,2931],{"class":201},"500",[146,2933,2934],{"class":156},", { \n",[146,2936,2937],{"class":148,"line":300},[146,2938,2939],{"class":156},"                    headers: { \n",[146,2941,2942,2945,2948,2951],{"class":148,"line":321},[146,2943,2944],{"class":163},"                        \"x-api-key\"",[146,2946,2947],{"class":156},": ",[146,2949,2950],{"class":201},"API_KEY",[146,2952,2953],{"class":156}," \n",[146,2955,2956],{"class":148,"line":355},[146,2957,2958],{"class":156},"                    } \n",[146,2960,2961],{"class":148,"line":562},[146,2962,2963],{"class":156},"                });\n",[146,2965,2966],{"class":148,"line":584},[146,2967,189],{"emptyLinePlaceholder":188},[146,2969,2970],{"class":148,"line":612},[146,2971,2972],{"class":2896},"                // transaction signing \n",[146,2974,2975],{"class":148,"line":618},[146,2976,189],{"emptyLinePlaceholder":188},[146,2978,2979,2982,2985,2987,2989,2991,2993,2996,2999,3002,3004,3006,3008,3010],{"class":148,"line":641},[146,2980,2981],{"class":152},"              const",[146,2983,2984],{"class":201}," executeResponse",[146,2986,205],{"class":152},[146,2988,1571],{"class":152},[146,2990,2463],{"class":208},[146,2992,212],{"class":156},[146,2994,2995],{"class":163},"`${",[146,2997,2998],{"class":201},"JUPITER_BASE_URL",[146,3000,3001],{"class":163},"}/execute`",[146,3003,507],{"class":156},[146,3005,2926],{"class":201},[146,3007,507],{"class":156},[146,3009,2931],{"class":201},[146,3011,218],{"class":156},[146,3013,3014,3017,3020],{"class":148,"line":672},[146,3015,3016],{"class":156},"                    method: ",[146,3018,3019],{"class":163},"\"POST\"",[146,3021,435],{"class":156},[146,3023,3024],{"class":148,"line":691},[146,3025,3026],{"class":156},"                    headers: {\n",[146,3028,3029,3032,3034,3037],{"class":148,"line":696},[146,3030,3031],{"class":163},"                        \"Content-Type\"",[146,3033,2947],{"class":156},[146,3035,3036],{"class":163},"\"application/json\"",[146,3038,435],{"class":156},[146,3040,3041,3043,3045,3047],{"class":148,"line":709},[146,3042,2944],{"class":163},[146,3044,2947],{"class":156},[146,3046,2950],{"class":201},[146,3048,435],{"class":156},[146,3050,3051],{"class":148,"line":721},[146,3052,3053],{"class":156},"                    },\n",[146,3055,3056,3059,3062,3064,3067],{"class":148,"line":758},[146,3057,3058],{"class":156},"                    body: ",[146,3060,3061],{"class":201},"JSON",[146,3063,33],{"class":156},[146,3065,3066],{"class":208},"stringify",[146,3068,1449],{"class":156},[146,3070,3071,3074,3076,3079,3082,3085,3088,3090,3093],{"class":148,"line":764},[146,3072,3073],{"class":156},"                        signedTransaction: Buffer.",[146,3075,160],{"class":208},[146,3077,3078],{"class":156},"(transaction.",[146,3080,3081],{"class":208},"serialize",[146,3083,3084],{"class":156},"()).",[146,3086,3087],{"class":208},"toString",[146,3089,212],{"class":156},[146,3091,3092],{"class":163},"\"base64\"",[146,3094,352],{"class":156},[146,3096,3097],{"class":148,"line":769},[146,3098,3099],{"class":156},"                        requestId: orderResult.requestId,\n",[146,3101,3102],{"class":148,"line":774},[146,3103,3104],{"class":156},"                    }),\n",[146,3106,3107],{"class":148,"line":813},[146,3108,2963],{"class":156},[146,3110,3111],{"class":148,"line":820},[146,3112,189],{"emptyLinePlaceholder":188},[146,3114,3115],{"class":148,"line":825},[146,3116,3117],{"class":156},"            })\n",[146,3119,3120],{"class":148,"line":840},[146,3121,3122],{"class":156},"        );\n",[146,3124,3125],{"class":148,"line":856},[146,3126,3127],{"class":2896},"         // Sleep after each chunk to avoid hitting rate limits on the next batch\n",[146,3129,3130,3133,3136,3138,3141],{"class":148,"line":871},[146,3131,3132],{"class":152},"         await",[146,3134,3135],{"class":208}," sleep",[146,3137,212],{"class":156},[146,3139,3140],{"class":201},"1000",[146,3142,559],{"class":156},[146,3144,3145],{"class":148,"line":895},[146,3146,1477],{"class":156},[10,3148,3149],{},"This combination of chunking the workload and adding retry logic lets the bot handle concurrent mass operations safely and reliably, even when relying entirely on public nodes.",[3151,3152,3153],"note",{},[10,3154,2403,3155,3162],{},[21,3156,3159],{"href":3157,"rel":3158},"https://docs.riavzon.com/docs/utils/shared/chunkprocess",[25],[143,3160,3161],{},"chunkProcess"," function comes also from my utils package",[10,3164,3165,3166,3169],{},"But EVEN THAT was not enough for the sell functions (massOut, and massSell) to succeed. Because at first the logic in these functions was heavily using the rpc's methods, then refactored to make a single call with the ",[143,3167,3168],{},"getProgramAccounts"," method to get all accounts and parse it locally (find the wallet, and the target mint), which also didn't work (rate limited), and was error prone.",[10,3171,3172],{},"So I implemented a persistent positions tracker, with the following schema:",[135,3174,3177],{"className":137,"code":3175,"filename":3176,"language":140,"meta":141,"style":141},"import { sql } from 'drizzle-orm';\nimport { sqliteTable, text, integer, numeric } from 'drizzle-orm/sqlite-core';\nimport { walletTable } from './wallets';\n\n\n\nexport const positionsTable = sqliteTable('positions', {\n  id: integer('id').primaryKey({ autoIncrement: true }),\n  walletAddress: text('wallet_address').notNull().references(() => walletTable.publicAddress, { onDelete: 'cascade' }),\n  targetMintAddress: text('target_mint_address').notNull(),\n  solAmounts: numeric('sol_amounts', { mode: 'bigint' }),\n  tokenAmount: numeric('token_amount', { mode: 'bigint' }),\n  walletRemainingBalance: numeric(\"wallet_remaining_balance\", { mode: 'bigint' }),\n  updatedAt: integer('updated_at', {mode: 'timestamp'}).default(sql`(cast(unixepoch() as int))`),\n});\n","buyPositions.ts",[143,3178,3179,3192,3206,3220,3224,3228,3232,3252,3273,3307,3325,3346,3364,3382,3411],{"__ignoreMap":141},[146,3180,3181,3183,3185,3187,3190],{"class":148,"line":149},[146,3182,153],{"class":152},[146,3184,175],{"class":156},[146,3186,160],{"class":152},[146,3188,3189],{"class":163}," 'drizzle-orm'",[146,3191,167],{"class":156},[146,3193,3194,3196,3199,3201,3204],{"class":148,"line":170},[146,3195,153],{"class":152},[146,3197,3198],{"class":156}," { sqliteTable, text, integer, numeric } ",[146,3200,160],{"class":152},[146,3202,3203],{"class":163}," 'drizzle-orm/sqlite-core'",[146,3205,167],{"class":156},[146,3207,3208,3210,3213,3215,3218],{"class":148,"line":185},[146,3209,153],{"class":152},[146,3211,3212],{"class":156}," { walletTable } ",[146,3214,160],{"class":152},[146,3216,3217],{"class":163}," './wallets'",[146,3219,167],{"class":156},[146,3221,3222],{"class":148,"line":192},[146,3223,189],{"emptyLinePlaceholder":188},[146,3225,3226],{"class":148,"line":221},[146,3227,189],{"emptyLinePlaceholder":188},[146,3229,3230],{"class":148,"line":256},[146,3231,189],{"emptyLinePlaceholder":188},[146,3233,3234,3236,3238,3241,3243,3245,3247,3250],{"class":148,"line":279},[146,3235,195],{"class":152},[146,3237,198],{"class":152},[146,3239,3240],{"class":201}," positionsTable",[146,3242,205],{"class":152},[146,3244,209],{"class":208},[146,3246,212],{"class":156},[146,3248,3249],{"class":163},"'positions'",[146,3251,218],{"class":156},[146,3253,3254,3257,3259,3261,3263,3265,3267,3269,3271],{"class":148,"line":300},[146,3255,3256],{"class":156},"  id: ",[146,3258,227],{"class":208},[146,3260,212],{"class":156},[146,3262,232],{"class":163},[146,3264,270],{"class":156},[146,3266,244],{"class":208},[146,3268,247],{"class":156},[146,3270,250],{"class":201},[146,3272,253],{"class":156},[146,3274,3275,3278,3280,3282,3285,3287,3289,3291,3294,3297,3299,3302,3305],{"class":148,"line":321},[146,3276,3277],{"class":156},"  walletAddress: ",[146,3279,262],{"class":208},[146,3281,212],{"class":156},[146,3283,3284],{"class":163},"'wallet_address'",[146,3286,270],{"class":156},[146,3288,316],{"class":208},[146,3290,338],{"class":156},[146,3292,3293],{"class":208},"references",[146,3295,3296],{"class":156},"(() ",[146,3298,1879],{"class":152},[146,3300,3301],{"class":156}," walletTable.publicAddress, { onDelete: ",[146,3303,3304],{"class":163},"'cascade'",[146,3306,253],{"class":156},[146,3308,3309,3312,3314,3316,3319,3321,3323],{"class":148,"line":355},[146,3310,3311],{"class":156},"  targetMintAddress: ",[146,3313,262],{"class":208},[146,3315,212],{"class":156},[146,3317,3318],{"class":163},"'target_mint_address'",[146,3320,270],{"class":156},[146,3322,316],{"class":208},[146,3324,276],{"class":156},[146,3326,3327,3330,3333,3335,3338,3341,3344],{"class":148,"line":562},[146,3328,3329],{"class":156},"  solAmounts: ",[146,3331,3332],{"class":208},"numeric",[146,3334,212],{"class":156},[146,3336,3337],{"class":163},"'sol_amounts'",[146,3339,3340],{"class":156},", { mode: ",[146,3342,3343],{"class":163},"'bigint'",[146,3345,253],{"class":156},[146,3347,3348,3351,3353,3355,3358,3360,3362],{"class":148,"line":584},[146,3349,3350],{"class":156},"  tokenAmount: ",[146,3352,3332],{"class":208},[146,3354,212],{"class":156},[146,3356,3357],{"class":163},"'token_amount'",[146,3359,3340],{"class":156},[146,3361,3343],{"class":163},[146,3363,253],{"class":156},[146,3365,3366,3369,3371,3373,3376,3378,3380],{"class":148,"line":612},[146,3367,3368],{"class":156},"  walletRemainingBalance: ",[146,3370,3332],{"class":208},[146,3372,212],{"class":156},[146,3374,3375],{"class":163},"\"wallet_remaining_balance\"",[146,3377,3340],{"class":156},[146,3379,3343],{"class":163},[146,3381,253],{"class":156},[146,3383,3384,3387,3389,3391,3394,3396,3399,3401,3403,3405,3407,3409],{"class":148,"line":618},[146,3385,3386],{"class":156},"  updatedAt: ",[146,3388,227],{"class":208},[146,3390,212],{"class":156},[146,3392,3393],{"class":163},"'updated_at'",[146,3395,235],{"class":156},[146,3397,3398],{"class":163},"'timestamp'",[146,3400,241],{"class":156},[146,3402,341],{"class":208},[146,3404,212],{"class":156},[146,3406,346],{"class":208},[146,3408,349],{"class":163},[146,3410,352],{"class":156},[146,3412,3413],{"class":148,"line":641},[146,3414,358],{"class":156},[10,3416,3417],{},"This schema completely eliminated the need to ask the RPC node, \"what tokens do these 50 wallets hold?\" Instead of relying on the network for state, the application tracks it locally.",[10,3419,3420,3421,3424],{},"Every time a ",[143,3422,3423],{},"massBuy"," operation succeeds on-chain, it immediately records the exact resulting position, down to the specific token amount received, into this local database:",[135,3426,3428],{"className":137,"code":3427,"filename":2813,"language":140,"meta":141,"style":141},"// inside the execution loop\nif (result.status === \"Success\") {\n    log.success(`Buy Success! https://solscan.io/tx/${result.signature}`);\n    try {\n        await db.insert(positionsTable).values({\n            walletAddress: wallet.publicKey.toBase58(),\n            targetMintAddress: targetMintAddress,\n            solAmounts: lamportsPerWallet,\n            walletRemainingBalance: BigInt(balance),\n            tokenAmount: BigInt(result.outputAmountResult), // The exact amount received\n        });\n    } catch (err) { \n        log.error(\"Database Error (Swap succeeded on-chain, but failed to record)\", err);\n    }\n}\n\n",[143,3429,3430,3435,3450,3474,3480,3495,3504,3509,3514,3525,3538,3542,3551,3566,3570],{"__ignoreMap":141},[146,3431,3432],{"class":148,"line":149},[146,3433,3434],{"class":2896},"// inside the execution loop\n",[146,3436,3437,3440,3443,3445,3448],{"class":148,"line":170},[146,3438,3439],{"class":152},"if",[146,3441,3442],{"class":156}," (result.status ",[146,3444,1239],{"class":152},[146,3446,3447],{"class":163}," \"Success\"",[146,3449,1171],{"class":156},[146,3451,3452,3455,3457,3459,3462,3465,3467,3470,3472],{"class":148,"line":185},[146,3453,3454],{"class":156},"    log.",[146,3456,1488],{"class":208},[146,3458,212],{"class":156},[146,3460,3461],{"class":163},"`Buy Success! https://solscan.io/tx/${",[146,3463,3464],{"class":156},"result",[146,3466,33],{"class":163},[146,3468,3469],{"class":156},"signature",[146,3471,753],{"class":163},[146,3473,559],{"class":156},[146,3475,3476,3478],{"class":148,"line":192},[146,3477,1763],{"class":152},[146,3479,424],{"class":156},[146,3481,3482,3484,3486,3488,3491,3493],{"class":148,"line":221},[146,3483,2055],{"class":152},[146,3485,1437],{"class":156},[146,3487,1440],{"class":208},[146,3489,3490],{"class":156},"(positionsTable).",[146,3492,1446],{"class":208},[146,3494,1449],{"class":156},[146,3496,3497,3500,3502],{"class":148,"line":256},[146,3498,3499],{"class":156},"            walletAddress: wallet.publicKey.",[146,3501,1457],{"class":208},[146,3503,276],{"class":156},[146,3505,3506],{"class":148,"line":279},[146,3507,3508],{"class":156},"            targetMintAddress: targetMintAddress,\n",[146,3510,3511],{"class":148,"line":300},[146,3512,3513],{"class":156},"            solAmounts: lamportsPerWallet,\n",[146,3515,3516,3519,3522],{"class":148,"line":321},[146,3517,3518],{"class":156},"            walletRemainingBalance: ",[146,3520,3521],{"class":208},"BigInt",[146,3523,3524],{"class":156},"(balance),\n",[146,3526,3527,3530,3532,3535],{"class":148,"line":355},[146,3528,3529],{"class":156},"            tokenAmount: ",[146,3531,3521],{"class":208},[146,3533,3534],{"class":156},"(result.outputAmountResult), ",[146,3536,3537],{"class":2896},"// The exact amount received\n",[146,3539,3540],{"class":148,"line":562},[146,3541,2092],{"class":156},[146,3543,3544,3546,3548],{"class":148,"line":584},[146,3545,2127],{"class":156},[146,3547,715],{"class":152},[146,3549,3550],{"class":156}," (err) { \n",[146,3552,3553,3556,3558,3560,3563],{"class":148,"line":612},[146,3554,3555],{"class":156},"        log.",[146,3557,1163],{"class":208},[146,3559,212],{"class":156},[146,3561,3562],{"class":163},"\"Database Error (Swap succeeded on-chain, but failed to record)\"",[146,3564,3565],{"class":156},", err);\n",[146,3567,3568],{"class":148,"line":618},[146,3569,938],{"class":156},[146,3571,3572],{"class":148,"line":641},[146,3573,1507],{"class":156},[10,3575,3576,3577,3580,3581,3584],{},"Then, when it is time to trigger a ",[143,3578,3579],{},"massSell"," or a panic ",[143,3582,3583],{},"massOut",", the bot skips the heavy network queries entirely. It simply reads the active positions directly from SQLite, matches them with the correct encrypted private keys, and goes straight to building the sell orders:",[135,3586,3589],{"className":137,"code":3587,"filename":3588,"language":140,"meta":141,"style":141},"const filters: SQL[] = [];\n  filters.push(eq(positionsTable.targetMintAddress, targetMintAddress));\n\n  // optional filtering logic if specific wallets were chosen\n\n  const limit = (chosenWallets && chosenWallets.length > 0) ? chosenWallets.length : numberOfWallets;\n  const positions = await db.select().from(positionsTable).where(and(...filters)).limit(limit);\n\n  if (positions.length === 0) {\n    log.warn(`No positions found in database for mint: ${targetMintAddress}`);\n    return { \n        ok: false, \n        reason: `No positions found in database for mint: ${targetMintAddress}`\n    };\n  }\n\n","sell.ts",[143,3590,3591,3610,3625,3629,3634,3638,3678,3720,3724,3741,3759,3766,3777,3789,3794],{"__ignoreMap":141},[146,3592,3593,3595,3598,3600,3603,3605,3607],{"class":148,"line":149},[146,3594,416],{"class":152},[146,3596,3597],{"class":201}," filters",[146,3599,501],{"class":152},[146,3601,3602],{"class":208}," SQL",[146,3604,1837],{"class":156},[146,3606,955],{"class":152},[146,3608,3609],{"class":156}," [];\n",[146,3611,3612,3615,3618,3620,3622],{"class":148,"line":170},[146,3613,3614],{"class":156},"  filters.",[146,3616,3617],{"class":208},"push",[146,3619,212],{"class":156},[146,3621,1590],{"class":208},[146,3623,3624],{"class":156},"(positionsTable.targetMintAddress, targetMintAddress));\n",[146,3626,3627],{"class":148,"line":185},[146,3628,189],{"emptyLinePlaceholder":188},[146,3630,3631],{"class":148,"line":192},[146,3632,3633],{"class":2896},"  // optional filtering logic if specific wallets were chosen\n",[146,3635,3636],{"class":148,"line":221},[146,3637,189],{"emptyLinePlaceholder":188},[146,3639,3640,3643,3646,3648,3651,3653,3656,3658,3661,3663,3665,3668,3670,3672,3675],{"class":148,"line":256},[146,3641,3642],{"class":152},"  const",[146,3644,3645],{"class":201}," limit",[146,3647,205],{"class":152},[146,3649,3650],{"class":156}," (chosenWallets ",[146,3652,1861],{"class":152},[146,3654,3655],{"class":156}," chosenWallets.",[146,3657,909],{"class":201},[146,3659,3660],{"class":152}," >",[146,3662,958],{"class":201},[146,3664,1876],{"class":156},[146,3666,3667],{"class":152},"?",[146,3669,3655],{"class":156},[146,3671,909],{"class":201},[146,3673,3674],{"class":152}," :",[146,3676,3677],{"class":156}," numberOfWallets;\n",[146,3679,3680,3682,3685,3687,3689,3691,3693,3695,3697,3699,3701,3703,3706,3708,3711,3714,3717],{"class":148,"line":279},[146,3681,3642],{"class":152},[146,3683,3684],{"class":201}," positions",[146,3686,205],{"class":152},[146,3688,1571],{"class":152},[146,3690,1437],{"class":156},[146,3692,1576],{"class":208},[146,3694,338],{"class":156},[146,3696,160],{"class":208},[146,3698,3490],{"class":156},[146,3700,1585],{"class":208},[146,3702,212],{"class":156},[146,3704,3705],{"class":208},"and",[146,3707,212],{"class":156},[146,3709,3710],{"class":152},"...",[146,3712,3713],{"class":156},"filters)).",[146,3715,3716],{"class":208},"limit",[146,3718,3719],{"class":156},"(limit);\n",[146,3721,3722],{"class":148,"line":300},[146,3723,189],{"emptyLinePlaceholder":188},[146,3725,3726,3729,3732,3734,3737,3739],{"class":148,"line":321},[146,3727,3728],{"class":152},"  if",[146,3730,3731],{"class":156}," (positions.",[146,3733,909],{"class":201},[146,3735,3736],{"class":152}," ===",[146,3738,958],{"class":201},[146,3740,1171],{"class":156},[146,3742,3743,3745,3747,3749,3752,3755,3757],{"class":148,"line":355},[146,3744,3454],{"class":156},[146,3746,2618],{"class":208},[146,3748,212],{"class":156},[146,3750,3751],{"class":163},"`No positions found in database for mint: ${",[146,3753,3754],{"class":156},"targetMintAddress",[146,3756,753],{"class":163},[146,3758,559],{"class":156},[146,3760,3761,3763],{"class":148,"line":562},[146,3762,699],{"class":152},[146,3764,3765],{"class":156}," { \n",[146,3767,3768,3771,3774],{"class":148,"line":584},[146,3769,3770],{"class":156},"        ok: ",[146,3772,3773],{"class":201},"false",[146,3775,3776],{"class":156},", \n",[146,3778,3779,3782,3784,3786],{"class":148,"line":612},[146,3780,3781],{"class":156},"        reason: ",[146,3783,3751],{"class":163},[146,3785,3754],{"class":156},[146,3787,3788],{"class":163},"}`\n",[146,3790,3791],{"class":148,"line":618},[146,3792,3793],{"class":156},"    };\n",[146,3795,3796],{"class":148,"line":641},[146,3797,761],{"class":156},[10,3799,3800,3801,1514,3804,3806],{},"Adding this local state lets the sell operations instantly know exactly which wallets hold the target mint and exactly how much they hold. This drastically reduced the total number of RPC requests, completely bypassing the rate-limit errors I was hitting with ",[143,3802,3803],{},"getParsedTokenAccountsByOwner",[143,3805,3168],{}," before even reaching the Jupiter API.",[10,3808,3809],{},"If a sell succeeds, the local database simply deletes that position row (or updates the balance if it was a partial sell percentage).",[10,3811,3812,3813,507,3815,507,3817,3819],{},"With the core already figured out, and tested locally with a small amount of SOL in a live environment, it was time to add that Volume bot. This part actually wasn't that challenging after those three core functions (",[143,3814,3423],{},[143,3816,3579],{},[143,3818,3583],{},") were in place. A volume bot simply needs to reuse these three functions in a continuous loop to generate trading activity.",[10,3821,3822],{},"The main challenge here shifted from blockchain logic to process management. I needed the volume bot to run continuously in the background, independent of the main CLI, without locking up the user's terminal.",[10,3824,3825,3826,3829],{},"To achieve this, I used Node's ",[143,3827,3828],{},"child_process.spawn"," to start a detached background worker. To keep things simple and avoid complex IPC (Inter-Process Communication) at this stage, I passed the configuration down to the worker via environment variables:",[135,3831,3834],{"className":137,"code":3832,"filename":3833,"language":140,"meta":141,"style":141},"import { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\n\nconst child = spawn(process.execPath, process.argv.slice(1), {\n    detached: true,\n    stdio: \"ignore\",\n    env: {\n        ...process.env,\n        SOLANA_BOTS_INTERNAL: \"volume-worker\",\n        TARGET_MINT: args.target,\n        WALLETS: args.wallets,\n        AMOUNT: args.amount,\n        PASSWORD: args.password,\n        INTERVAL: args.interval,\n        TTL: args.ttl ?? \"\",\n    },\n});\n\nchild.unref();\n\nawait fs.writeFile(PID_FILE, String(child.pid));\nconsola.success(`Volume bot started in background. PID: ${child.pid}`);\n\n","main.ts",[143,3835,3836,3850,3864,3868,3893,3902,3912,3917,3925,3935,3940,3945,3950,3955,3960,3973,3978,3982,3986,3996,4000,4021],{"__ignoreMap":141},[146,3837,3838,3840,3843,3845,3848],{"class":148,"line":149},[146,3839,153],{"class":152},[146,3841,3842],{"class":156}," { spawn } ",[146,3844,160],{"class":152},[146,3846,3847],{"class":163}," 'node:child_process'",[146,3849,167],{"class":156},[146,3851,3852,3854,3857,3859,3862],{"class":148,"line":170},[146,3853,153],{"class":152},[146,3855,3856],{"class":156}," fs ",[146,3858,160],{"class":152},[146,3860,3861],{"class":163}," 'node:fs/promises'",[146,3863,167],{"class":156},[146,3865,3866],{"class":148,"line":185},[146,3867,189],{"emptyLinePlaceholder":188},[146,3869,3870,3872,3875,3877,3880,3883,3886,3888,3890],{"class":148,"line":192},[146,3871,416],{"class":152},[146,3873,3874],{"class":201}," child",[146,3876,205],{"class":152},[146,3878,3879],{"class":208}," spawn",[146,3881,3882],{"class":156},"(process.execPath, process.argv.",[146,3884,3885],{"class":208},"slice",[146,3887,212],{"class":156},[146,3889,464],{"class":201},[146,3891,3892],{"class":156},"), {\n",[146,3894,3895,3898,3900],{"class":148,"line":221},[146,3896,3897],{"class":156},"    detached: ",[146,3899,250],{"class":201},[146,3901,435],{"class":156},[146,3903,3904,3907,3910],{"class":148,"line":256},[146,3905,3906],{"class":156},"    stdio: ",[146,3908,3909],{"class":163},"\"ignore\"",[146,3911,435],{"class":156},[146,3913,3914],{"class":148,"line":279},[146,3915,3916],{"class":156},"    env: {\n",[146,3918,3919,3922],{"class":148,"line":300},[146,3920,3921],{"class":152},"        ...",[146,3923,3924],{"class":156},"process.env,\n",[146,3926,3927,3930,3933],{"class":148,"line":321},[146,3928,3929],{"class":156},"        SOLANA_BOTS_INTERNAL: ",[146,3931,3932],{"class":163},"\"volume-worker\"",[146,3934,435],{"class":156},[146,3936,3937],{"class":148,"line":355},[146,3938,3939],{"class":156},"        TARGET_MINT: args.target,\n",[146,3941,3942],{"class":148,"line":562},[146,3943,3944],{"class":156},"        WALLETS: args.wallets,\n",[146,3946,3947],{"class":148,"line":584},[146,3948,3949],{"class":156},"        AMOUNT: args.amount,\n",[146,3951,3952],{"class":148,"line":612},[146,3953,3954],{"class":156},"        PASSWORD: args.password,\n",[146,3956,3957],{"class":148,"line":618},[146,3958,3959],{"class":156},"        INTERVAL: args.interval,\n",[146,3961,3962,3965,3968,3971],{"class":148,"line":641},[146,3963,3964],{"class":156},"        TTL: args.ttl ",[146,3966,3967],{"class":152},"??",[146,3969,3970],{"class":163}," \"\"",[146,3972,435],{"class":156},[146,3974,3975],{"class":148,"line":672},[146,3976,3977],{"class":156},"    },\n",[146,3979,3980],{"class":148,"line":691},[146,3981,358],{"class":156},[146,3983,3984],{"class":148,"line":696},[146,3985,189],{"emptyLinePlaceholder":188},[146,3987,3988,3991,3994],{"class":148,"line":709},[146,3989,3990],{"class":156},"child.",[146,3992,3993],{"class":208},"unref",[146,3995,688],{"class":156},[146,3997,3998],{"class":148,"line":721},[146,3999,189],{"emptyLinePlaceholder":188},[146,4001,4002,4004,4006,4009,4011,4014,4016,4018],{"class":148,"line":758},[146,4003,2823],{"class":152},[146,4005,1688],{"class":156},[146,4007,4008],{"class":208},"writeFile",[146,4010,212],{"class":156},[146,4012,4013],{"class":201},"PID_FILE",[146,4015,507],{"class":156},[146,4017,2920],{"class":208},[146,4019,4020],{"class":156},"(child.pid));\n",[146,4022,4023,4026,4028,4030,4033,4036,4038,4041,4043],{"class":148,"line":764},[146,4024,4025],{"class":156},"consola.",[146,4027,1488],{"class":208},[146,4029,212],{"class":156},[146,4031,4032],{"class":163},"`Volume bot started in background. PID: ${",[146,4034,4035],{"class":156},"child",[146,4037,33],{"class":163},[146,4039,4040],{"class":156},"pid",[146,4042,753],{"class":163},[146,4044,559],{"class":156},[10,4046,4047,4048,4051,4052,4055],{},"By setting ",[143,4049,4050],{},"detached: true"," and calling ",[143,4053,4054],{},"child.unref()",", the worker detaches from the parent terminal. I then save its Process ID to a local .pid file so the CLI can keep track of it and shut it down later.",[10,4057,4058],{},"Inside the worker process itself, it runs a continuous while loop until a specified TTL expires or it receives a stop signal. But I couldn't just grab the same wallets every single loop, if a user has 50 wallets but only wants to run volume with 5 at a time, the bot should cycle through them to distribute the trading activity naturally.",[10,4060,4061,4062,4065],{},"To solve this, I implemented a round-robin rotation logic using a ",[143,4063,4064],{},"walletOffset"," index. In each cycle, it grabs a pool of eligible wallets (wallets that actually have enough SOL to cover the trade amount plus the network fees), and then slices out the requested batch:",[135,4067,4070],{"className":137,"code":4068,"filename":4069,"language":140,"meta":141,"style":141},"let walletOffset = 0;\n\nwhile (!stopping && Date.now() \u003C expiresAt) {\n    // Fetch all wallets that can afford the trade + fees\n    const pool = await getEligibleWallets(password, maxSolPerWallet + NETWORK_FEE_BUFFER);\n    \n    if (pool.length \u003C numberOfWallets) {\n        consola.error(`Only ${pool.length} wallets have enough SOL. Need ${numberOfWallets}.`);\n        await sleep(5000);\n        continue; \n    }\n\n    // rotation logic\n    const selectedWallets = [];\n    for (let i = 0; i \u003C numberOfWallets; i++) {\n        const index = (walletOffset + i) % pool.length;\n        selectedWallets.push(pool[index]);\n    }\n    \n    // Shift offset\n    walletOffset = (walletOffset + numberOfWallets) % pool.length;\n\n    const selectedKeypairs = selectedWallets.map(entry => entry.wallet);\n\n    try {\n        // 3. Execute Mass Buy\n        const buyResult = await massBuy(targetMintAddress, maxSolPerWallet, numberOfWallets, password, 1, selectedKeypairs);\n\n    } catch (err) {\n\n    }\n\n    // Mass Sell\n\n    await sleep(interval ?? 0);\n}\n","volumeBotWorker.ts",[143,4071,4072,4086,4090,4118,4123,4147,4151,4165,4194,4207,4214,4218,4222,4227,4238,4267,4294,4304,4308,4312,4317,4339,4343,4367,4371,4377,4382,4404,4408,4416,4420,4424,4428,4433,4437,4453],{"__ignoreMap":141},[146,4073,4074,4077,4080,4082,4084],{"class":148,"line":149},[146,4075,4076],{"class":152},"let",[146,4078,4079],{"class":156}," walletOffset ",[146,4081,955],{"class":152},[146,4083,958],{"class":201},[146,4085,167],{"class":156},[146,4087,4088],{"class":148,"line":170},[146,4089,189],{"emptyLinePlaceholder":188},[146,4091,4092,4095,4097,4099,4102,4104,4107,4110,4112,4115],{"class":148,"line":185},[146,4093,4094],{"class":152},"while",[146,4096,494],{"class":156},[146,4098,1712],{"class":152},[146,4100,4101],{"class":156},"stopping ",[146,4103,1861],{"class":152},[146,4105,4106],{"class":156}," Date.",[146,4108,4109],{"class":208},"now",[146,4111,2576],{"class":156},[146,4113,4114],{"class":152},"\u003C",[146,4116,4117],{"class":156}," expiresAt) {\n",[146,4119,4120],{"class":148,"line":192},[146,4121,4122],{"class":2896},"    // Fetch all wallets that can afford the trade + fees\n",[146,4124,4125,4127,4130,4132,4134,4137,4140,4142,4145],{"class":148,"line":221},[146,4126,540],{"class":152},[146,4128,4129],{"class":201}," pool",[146,4131,205],{"class":152},[146,4133,1571],{"class":152},[146,4135,4136],{"class":208}," getEligibleWallets",[146,4138,4139],{"class":156},"(password, maxSolPerWallet ",[146,4141,884],{"class":152},[146,4143,4144],{"class":201}," NETWORK_FEE_BUFFER",[146,4146,559],{"class":156},[146,4148,4149],{"class":148,"line":256},[146,4150,615],{"class":156},[146,4152,4153,4155,4158,4160,4162],{"class":148,"line":279},[146,4154,903],{"class":152},[146,4156,4157],{"class":156}," (pool.",[146,4159,909],{"class":201},[146,4161,912],{"class":152},[146,4163,4164],{"class":156}," numberOfWallets) {\n",[146,4166,4167,4169,4171,4173,4176,4179,4181,4183,4186,4189,4192],{"class":148,"line":300},[146,4168,1620],{"class":156},[146,4170,1163],{"class":208},[146,4172,212],{"class":156},[146,4174,4175],{"class":163},"`Only ${",[146,4177,4178],{"class":156},"pool",[146,4180,33],{"class":163},[146,4182,909],{"class":201},[146,4184,4185],{"class":163},"} wallets have enough SOL. Need ${",[146,4187,4188],{"class":156},"numberOfWallets",[146,4190,4191],{"class":163},"}.`",[146,4193,559],{"class":156},[146,4195,4196,4198,4200,4202,4205],{"class":148,"line":321},[146,4197,2055],{"class":152},[146,4199,3135],{"class":208},[146,4201,212],{"class":156},[146,4203,4204],{"class":201},"5000",[146,4206,559],{"class":156},[146,4208,4209,4212],{"class":148,"line":355},[146,4210,4211],{"class":152},"        continue",[146,4213,853],{"class":156},[146,4215,4216],{"class":148,"line":562},[146,4217,938],{"class":156},[146,4219,4220],{"class":148,"line":584},[146,4221,189],{"emptyLinePlaceholder":188},[146,4223,4224],{"class":148,"line":612},[146,4225,4226],{"class":2896},"    // rotation logic\n",[146,4228,4229,4231,4234,4236],{"class":148,"line":618},[146,4230,540],{"class":152},[146,4232,4233],{"class":201}," selectedWallets",[146,4235,205],{"class":152},[146,4237,3609],{"class":156},[146,4239,4240,4243,4245,4247,4250,4252,4254,4257,4259,4262,4265],{"class":148,"line":641},[146,4241,4242],{"class":152},"    for",[146,4244,494],{"class":156},[146,4246,4076],{"class":152},[146,4248,4249],{"class":156}," i ",[146,4251,955],{"class":152},[146,4253,958],{"class":201},[146,4255,4256],{"class":156},"; i ",[146,4258,4114],{"class":152},[146,4260,4261],{"class":156}," numberOfWallets; i",[146,4263,4264],{"class":152},"++",[146,4266,1171],{"class":156},[146,4268,4269,4271,4274,4276,4279,4281,4284,4287,4290,4292],{"class":148,"line":672},[146,4270,1770],{"class":152},[146,4272,4273],{"class":201}," index",[146,4275,205],{"class":152},[146,4277,4278],{"class":156}," (walletOffset ",[146,4280,884],{"class":152},[146,4282,4283],{"class":156}," i) ",[146,4285,4286],{"class":152},"%",[146,4288,4289],{"class":156}," pool.",[146,4291,909],{"class":201},[146,4293,167],{"class":156},[146,4295,4296,4299,4301],{"class":148,"line":691},[146,4297,4298],{"class":156},"        selectedWallets.",[146,4300,3617],{"class":208},[146,4302,4303],{"class":156},"(pool[index]);\n",[146,4305,4306],{"class":148,"line":696},[146,4307,938],{"class":156},[146,4309,4310],{"class":148,"line":709},[146,4311,615],{"class":156},[146,4313,4314],{"class":148,"line":721},[146,4315,4316],{"class":2896},"    // Shift offset\n",[146,4318,4319,4322,4324,4326,4328,4331,4333,4335,4337],{"class":148,"line":758},[146,4320,4321],{"class":156},"    walletOffset ",[146,4323,955],{"class":152},[146,4325,4278],{"class":156},[146,4327,884],{"class":152},[146,4329,4330],{"class":156}," numberOfWallets) ",[146,4332,4286],{"class":152},[146,4334,4289],{"class":156},[146,4336,909],{"class":201},[146,4338,167],{"class":156},[146,4340,4341],{"class":148,"line":764},[146,4342,189],{"emptyLinePlaceholder":188},[146,4344,4345,4347,4350,4352,4355,4357,4359,4362,4364],{"class":148,"line":769},[146,4346,540],{"class":152},[146,4348,4349],{"class":201}," selectedKeypairs",[146,4351,205],{"class":152},[146,4353,4354],{"class":156}," selectedWallets.",[146,4356,2865],{"class":208},[146,4358,212],{"class":156},[146,4360,4361],{"class":497},"entry",[146,4363,526],{"class":152},[146,4365,4366],{"class":156}," entry.wallet);\n",[146,4368,4369],{"class":148,"line":774},[146,4370,189],{"emptyLinePlaceholder":188},[146,4372,4373,4375],{"class":148,"line":813},[146,4374,1763],{"class":152},[146,4376,424],{"class":156},[146,4378,4379],{"class":148,"line":820},[146,4380,4381],{"class":2896},"        // 3. Execute Mass Buy\n",[146,4383,4384,4386,4389,4391,4393,4396,4399,4401],{"class":148,"line":825},[146,4385,1770],{"class":152},[146,4387,4388],{"class":201}," buyResult",[146,4390,205],{"class":152},[146,4392,1571],{"class":152},[146,4394,4395],{"class":208}," massBuy",[146,4397,4398],{"class":156},"(targetMintAddress, maxSolPerWallet, numberOfWallets, password, ",[146,4400,464],{"class":201},[146,4402,4403],{"class":156},", selectedKeypairs);\n",[146,4405,4406],{"class":148,"line":840},[146,4407,189],{"emptyLinePlaceholder":188},[146,4409,4410,4412,4414],{"class":148,"line":856},[146,4411,2127],{"class":156},[146,4413,715],{"class":152},[146,4415,718],{"class":156},[146,4417,4418],{"class":148,"line":871},[146,4419,189],{"emptyLinePlaceholder":188},[146,4421,4422],{"class":148,"line":895},[146,4423,938],{"class":156},[146,4425,4426],{"class":148,"line":900},[146,4427,189],{"emptyLinePlaceholder":188},[146,4429,4430],{"class":148,"line":918},[146,4431,4432],{"class":2896},"    // Mass Sell\n",[146,4434,4435],{"class":148,"line":935},[146,4436,189],{"emptyLinePlaceholder":188},[146,4438,4439,4442,4444,4447,4449,4451],{"class":148,"line":941},[146,4440,4441],{"class":152},"    await",[146,4443,3135],{"class":208},[146,4445,4446],{"class":156},"(interval ",[146,4448,3967],{"class":152},[146,4450,958],{"class":201},[146,4452,559],{"class":156},[146,4454,4455],{"class":148,"line":946},[146,4456,1507],{"class":156},[10,4458,4459,4460,4462],{},"Using the modulo operator, the ",[143,4461,4064],{}," seamlessly wraps back to the beginning of the array once it reaches the end. This ensures every funded sub-wallet gets used evenly, creating much more organic-looking volume on-chain.",[10,4464,4465,4466,4469,4470,4473],{},"Because the worker is detached and has no standard output to the terminal, it continuously writes its current status (buy/sell counts, P&L, current action) to a local ",[143,4467,4468],{},"volumeBot.stats.json"," file. The CLI can then read this file via a ",[143,4471,4472],{},"volume-status"," command to show updates.",[10,4475,4476],{},"Trading with real money means you can't just forcefully kill a process midway through a loop. You might leave wallets holding positions that will crash.",[10,4478,4479,4480,4483,4484,4487],{},"When the user issues the stop command, the CLI sends a ",[143,4481,4482],{},"SIGTERM"," signal to the saved process ID. The worker script listens for this exact signal, flips the stopping boolean to true, which breaks the while loop, and executes a final ",[143,4485,4486],{},"massOutForAllWallets"," to panic-sell and clean up any lingering positions before fully shutting down:",[135,4489,4491],{"className":137,"code":4490,"filename":4069,"language":140,"meta":141,"style":141},"process.on('SIGTERM', () => {\n  stopping = true; \n  await fs.unlink(PID_FILE).catch(() => {});\n});\n\nfinally {\n    if (shouldMassOut) {\n        try {\n            consola.info(\"Closing all positions before shutdown...\");\n            await massOutForAllWallets(password);\n            consola.success(\"Mass out completed.\");\n        } catch (err) {\n            consola.error(\"Mass out failed:\", err);\n        }\n    }\n    await cleanup();\n}\n",[143,4492,4493,4513,4525,4550,4554,4558,4565,4572,4578,4593,4604,4617,4625,4638,4642,4646,4655],{"__ignoreMap":141},[146,4494,4495,4498,4501,4503,4506,4509,4511],{"class":148,"line":149},[146,4496,4497],{"class":156},"process.",[146,4499,4500],{"class":208},"on",[146,4502,212],{"class":156},[146,4504,4505],{"class":163},"'SIGTERM'",[146,4507,4508],{"class":156},", () ",[146,4510,1879],{"class":152},[146,4512,424],{"class":156},[146,4514,4515,4518,4520,4523],{"class":148,"line":170},[146,4516,4517],{"class":156},"  stopping ",[146,4519,955],{"class":152},[146,4521,4522],{"class":201}," true",[146,4524,853],{"class":156},[146,4526,4527,4530,4532,4535,4537,4539,4541,4543,4545,4547],{"class":148,"line":185},[146,4528,4529],{"class":152},"  await",[146,4531,1688],{"class":156},[146,4533,4534],{"class":208},"unlink",[146,4536,212],{"class":156},[146,4538,4013],{"class":201},[146,4540,270],{"class":156},[146,4542,715],{"class":208},[146,4544,3296],{"class":156},[146,4546,1879],{"class":152},[146,4548,4549],{"class":156}," {});\n",[146,4551,4552],{"class":148,"line":192},[146,4553,358],{"class":156},[146,4555,4556],{"class":148,"line":221},[146,4557,189],{"emptyLinePlaceholder":188},[146,4559,4560,4563],{"class":148,"line":256},[146,4561,4562],{"class":152},"finally",[146,4564,424],{"class":156},[146,4566,4567,4569],{"class":148,"line":279},[146,4568,903],{"class":152},[146,4570,4571],{"class":156}," (shouldMassOut) {\n",[146,4573,4574,4576],{"class":148,"line":300},[146,4575,1806],{"class":152},[146,4577,424],{"class":156},[146,4579,4580,4583,4586,4588,4591],{"class":148,"line":321},[146,4581,4582],{"class":156},"            consola.",[146,4584,4585],{"class":208},"info",[146,4587,212],{"class":156},[146,4589,4590],{"class":163},"\"Closing all positions before shutdown...\"",[146,4592,559],{"class":156},[146,4594,4595,4598,4601],{"class":148,"line":355},[146,4596,4597],{"class":152},"            await",[146,4599,4600],{"class":208}," massOutForAllWallets",[146,4602,4603],{"class":156},"(password);\n",[146,4605,4606,4608,4610,4612,4615],{"class":148,"line":562},[146,4607,4582],{"class":156},[146,4609,1488],{"class":208},[146,4611,212],{"class":156},[146,4613,4614],{"class":163},"\"Mass out completed.\"",[146,4616,559],{"class":156},[146,4618,4619,4621,4623],{"class":148,"line":584},[146,4620,1989],{"class":156},[146,4622,715],{"class":152},[146,4624,718],{"class":156},[146,4626,4627,4629,4631,4633,4636],{"class":148,"line":612},[146,4628,4582],{"class":156},[146,4630,1163],{"class":208},[146,4632,212],{"class":156},[146,4634,4635],{"class":163},"\"Mass out failed:\"",[146,4637,3565],{"class":156},[146,4639,4640],{"class":148,"line":618},[146,4641,2012],{"class":156},[146,4643,4644],{"class":148,"line":641},[146,4645,938],{"class":156},[146,4647,4648,4650,4653],{"class":148,"line":672},[146,4649,4441],{"class":152},[146,4651,4652],{"class":208}," cleanup",[146,4654,688],{"class":156},[146,4656,4657],{"class":148,"line":691},[146,4658,1507],{"class":156},[10,4660,4661,4662,4664],{},"With all that was all set, I defined the cli commands with ",[143,4663,132],{}," which is pretty straightforward. Example of the volume bot's start command:",[135,4666,4669],{"className":137,"code":4667,"filename":4668,"language":140,"meta":141,"style":141},"const dataDir =\n  process.platform === \"win32\"\n    ? path.join(process.env[\"APPDATA\"] ?? path.join(os.homedir(), 'AppData', 'Roaming'), \"SolanaBot\")\n    : path.join(os.homedir(), \".solana-bot\");\n\nconst PID_FILE = path.join(dataDir, 'volumeBot.pid');\n\nexport const volumeStart = defineCommand({\n  meta: {\n    name: 'Start Volume Bot',\n    description: 'Start the volume bot in the background',\n  },\n  args: {\n    target: {\n      type: \"string\",\n      required: true,\n      description: \"The SPL token mint address to target (The coin).\",\n      valueHint: \"...\"\n    },\n    wallets: {\n      type: \"string\",\n      required: false,\n      default: \"1\",\n      description: \"Number of sub-wallets to use for volume trading.\",\n      valueHint: \"1\"\n    },\n    amount: {\n      type: \"string\",\n      required: true,\n      description: \"Amount of SOL to use per trade for each wallet.\",\n      default: \"0.01\",\n      valueHint: \"0.01\"\n    },\n    password: {\n      type: \"string\",\n      required: true,\n      description: \"Master password used to decrypt your stored wallets.\",\n      valueHint: \"mySecurePassword\"\n    },\n    interval: {\n      type: \"string\",\n      required: false,\n      description: \"Run a cycle every this much time (ms)\",\n      valueHint: \"1000 for every 1s (default)\"\n    },\n    ttl: {\n      type: \"string\",\n      required: false,\n      default: \"60\",\n      description: \"Optional time-to-live for the bot to run in seconds. if not added the bot will stop after 60s\",\n      valueHint: \"60\"\n    },\n  },\n\n  async run({ args }) {\n    try {\n          const existingPid = Number(await fs.readFile(PID_FILE, 'utf8'));\n          process.kill(existingPid, 0);\n          consola.warn(`Volume bot already running: ${existingPid}`);\n          return;\n    } catch { }\n    \n        await fs.mkdir(dataDir, { recursive: true });\n        const child = spawn(process.execPath, process.argv.slice(1), {\n            detached: true,\n            stdio: \"ignore\",\n            env: {\n                ...process.env,\n                SOLANA_BOTS_INTERNAL: \"volume-worker\",\n                TARGET_MINT: args.target,\n                WALLETS: args.wallets,\n                AMOUNT: args.amount,\n                PASSWORD: args.password,\n                INTERVAL: args.interval,\n                TTL: args.ttl ?? \"\",\n            },\n        });\n\n        child.unref();\n\n        await fs.writeFile(PID_FILE, String(child.pid));\n        consola.success(`Volume bot started in background. PID: ${child.pid}`);\n  },\n  \n});\n","src/lib/transactions/volumeBot/volumeStart.ts",[143,4670,4671,4681,4691,4741,4761,4765,4786,4790,4806,4811,4821,4831,4836,4841,4846,4856,4865,4875,4883,4887,4892,4900,4908,4918,4927,4934,4938,4943,4951,4959,4968,4977,4984,4988,4993,5001,5009,5018,5025,5029,5034,5042,5050,5059,5066,5070,5075,5083,5091,5100,5109,5116,5120,5124,5128,5145,5151,5184,5200,5219,5227,5237,5242,5260,5281,5291,5301,5307,5315,5325,5331,5337,5343,5349,5355,5367,5373,5378,5383,5393,5398,5417,5438,5443,5449],{"__ignoreMap":141},[146,4672,4673,4675,4678],{"class":148,"line":149},[146,4674,416],{"class":152},[146,4676,4677],{"class":201}," dataDir",[146,4679,4680],{"class":152}," =\n",[146,4682,4683,4686,4688],{"class":148,"line":170},[146,4684,4685],{"class":156},"  process.platform ",[146,4687,1239],{"class":152},[146,4689,4690],{"class":163}," \"win32\"\n",[146,4692,4693,4696,4698,4701,4704,4707,4709,4711,4713,4715,4718,4721,4724,4727,4729,4732,4735,4738],{"class":148,"line":185},[146,4694,4695],{"class":152},"    ?",[146,4697,1664],{"class":156},[146,4699,4700],{"class":208},"join",[146,4702,4703],{"class":156},"(process.env[",[146,4705,4706],{"class":163},"\"APPDATA\"",[146,4708,1566],{"class":156},[146,4710,3967],{"class":152},[146,4712,1664],{"class":156},[146,4714,4700],{"class":208},[146,4716,4717],{"class":156},"(os.",[146,4719,4720],{"class":208},"homedir",[146,4722,4723],{"class":156},"(), ",[146,4725,4726],{"class":163},"'AppData'",[146,4728,507],{"class":156},[146,4730,4731],{"class":163},"'Roaming'",[146,4733,4734],{"class":156},"), ",[146,4736,4737],{"class":163},"\"SolanaBot\"",[146,4739,4740],{"class":156},")\n",[146,4742,4743,4746,4748,4750,4752,4754,4756,4759],{"class":148,"line":192},[146,4744,4745],{"class":152},"    :",[146,4747,1664],{"class":156},[146,4749,4700],{"class":208},[146,4751,4717],{"class":156},[146,4753,4720],{"class":208},[146,4755,4723],{"class":156},[146,4757,4758],{"class":163},"\".solana-bot\"",[146,4760,559],{"class":156},[146,4762,4763],{"class":148,"line":221},[146,4764,189],{"emptyLinePlaceholder":188},[146,4766,4767,4769,4772,4774,4776,4778,4781,4784],{"class":148,"line":256},[146,4768,416],{"class":152},[146,4770,4771],{"class":201}," PID_FILE",[146,4773,205],{"class":152},[146,4775,1664],{"class":156},[146,4777,4700],{"class":208},[146,4779,4780],{"class":156},"(dataDir, ",[146,4782,4783],{"class":163},"'volumeBot.pid'",[146,4785,559],{"class":156},[146,4787,4788],{"class":148,"line":279},[146,4789,189],{"emptyLinePlaceholder":188},[146,4791,4792,4794,4796,4799,4801,4804],{"class":148,"line":300},[146,4793,195],{"class":152},[146,4795,198],{"class":152},[146,4797,4798],{"class":201}," volumeStart",[146,4800,205],{"class":152},[146,4802,4803],{"class":208}," defineCommand",[146,4805,1449],{"class":156},[146,4807,4808],{"class":148,"line":321},[146,4809,4810],{"class":156},"  meta: {\n",[146,4812,4813,4816,4819],{"class":148,"line":355},[146,4814,4815],{"class":156},"    name: ",[146,4817,4818],{"class":163},"'Start Volume Bot'",[146,4820,435],{"class":156},[146,4822,4823,4826,4829],{"class":148,"line":562},[146,4824,4825],{"class":156},"    description: ",[146,4827,4828],{"class":163},"'Start the volume bot in the background'",[146,4830,435],{"class":156},[146,4832,4833],{"class":148,"line":584},[146,4834,4835],{"class":156},"  },\n",[146,4837,4838],{"class":148,"line":612},[146,4839,4840],{"class":156},"  args: {\n",[146,4842,4843],{"class":148,"line":618},[146,4844,4845],{"class":156},"    target: {\n",[146,4847,4848,4851,4854],{"class":148,"line":641},[146,4849,4850],{"class":156},"      type: ",[146,4852,4853],{"class":163},"\"string\"",[146,4855,435],{"class":156},[146,4857,4858,4861,4863],{"class":148,"line":672},[146,4859,4860],{"class":156},"      required: ",[146,4862,250],{"class":201},[146,4864,435],{"class":156},[146,4866,4867,4870,4873],{"class":148,"line":691},[146,4868,4869],{"class":156},"      description: ",[146,4871,4872],{"class":163},"\"The SPL token mint address to target (The coin).\"",[146,4874,435],{"class":156},[146,4876,4877,4880],{"class":148,"line":696},[146,4878,4879],{"class":156},"      valueHint: ",[146,4881,4882],{"class":163},"\"...\"\n",[146,4884,4885],{"class":148,"line":709},[146,4886,3977],{"class":156},[146,4888,4889],{"class":148,"line":721},[146,4890,4891],{"class":156},"    wallets: {\n",[146,4893,4894,4896,4898],{"class":148,"line":758},[146,4895,4850],{"class":156},[146,4897,4853],{"class":163},[146,4899,435],{"class":156},[146,4901,4902,4904,4906],{"class":148,"line":764},[146,4903,4860],{"class":156},[146,4905,3773],{"class":201},[146,4907,435],{"class":156},[146,4909,4910,4913,4916],{"class":148,"line":769},[146,4911,4912],{"class":156},"      default: ",[146,4914,4915],{"class":163},"\"1\"",[146,4917,435],{"class":156},[146,4919,4920,4922,4925],{"class":148,"line":774},[146,4921,4869],{"class":156},[146,4923,4924],{"class":163},"\"Number of sub-wallets to use for volume trading.\"",[146,4926,435],{"class":156},[146,4928,4929,4931],{"class":148,"line":813},[146,4930,4879],{"class":156},[146,4932,4933],{"class":163},"\"1\"\n",[146,4935,4936],{"class":148,"line":820},[146,4937,3977],{"class":156},[146,4939,4940],{"class":148,"line":825},[146,4941,4942],{"class":156},"    amount: {\n",[146,4944,4945,4947,4949],{"class":148,"line":840},[146,4946,4850],{"class":156},[146,4948,4853],{"class":163},[146,4950,435],{"class":156},[146,4952,4953,4955,4957],{"class":148,"line":856},[146,4954,4860],{"class":156},[146,4956,250],{"class":201},[146,4958,435],{"class":156},[146,4960,4961,4963,4966],{"class":148,"line":871},[146,4962,4869],{"class":156},[146,4964,4965],{"class":163},"\"Amount of SOL to use per trade for each wallet.\"",[146,4967,435],{"class":156},[146,4969,4970,4972,4975],{"class":148,"line":895},[146,4971,4912],{"class":156},[146,4973,4974],{"class":163},"\"0.01\"",[146,4976,435],{"class":156},[146,4978,4979,4981],{"class":148,"line":900},[146,4980,4879],{"class":156},[146,4982,4983],{"class":163},"\"0.01\"\n",[146,4985,4986],{"class":148,"line":918},[146,4987,3977],{"class":156},[146,4989,4990],{"class":148,"line":935},[146,4991,4992],{"class":156},"    password: {\n",[146,4994,4995,4997,4999],{"class":148,"line":941},[146,4996,4850],{"class":156},[146,4998,4853],{"class":163},[146,5000,435],{"class":156},[146,5002,5003,5005,5007],{"class":148,"line":946},[146,5004,4860],{"class":156},[146,5006,250],{"class":201},[146,5008,435],{"class":156},[146,5010,5011,5013,5016],{"class":148,"line":963},[146,5012,4869],{"class":156},[146,5014,5015],{"class":163},"\"Master password used to decrypt your stored wallets.\"",[146,5017,435],{"class":156},[146,5019,5020,5022],{"class":148,"line":987},[146,5021,4879],{"class":156},[146,5023,5024],{"class":163},"\"mySecurePassword\"\n",[146,5026,5027],{"class":148,"line":1007},[146,5028,3977],{"class":156},[146,5030,5031],{"class":148,"line":1027},[146,5032,5033],{"class":156},"    interval: {\n",[146,5035,5036,5038,5040],{"class":148,"line":1044},[146,5037,4850],{"class":156},[146,5039,4853],{"class":163},[146,5041,435],{"class":156},[146,5043,5044,5046,5048],{"class":148,"line":1049},[146,5045,4860],{"class":156},[146,5047,3773],{"class":201},[146,5049,435],{"class":156},[146,5051,5052,5054,5057],{"class":148,"line":1072},[146,5053,4869],{"class":156},[146,5055,5056],{"class":163},"\"Run a cycle every this much time (ms)\"",[146,5058,435],{"class":156},[146,5060,5061,5063],{"class":148,"line":1093},[146,5062,4879],{"class":156},[146,5064,5065],{"class":163},"\"1000 for every 1s (default)\"\n",[146,5067,5068],{"class":148,"line":1105},[146,5069,3977],{"class":156},[146,5071,5072],{"class":148,"line":1110},[146,5073,5074],{"class":156},"    ttl: {\n",[146,5076,5077,5079,5081],{"class":148,"line":1127},[146,5078,4850],{"class":156},[146,5080,4853],{"class":163},[146,5082,435],{"class":156},[146,5084,5085,5087,5089],{"class":148,"line":1138},[146,5086,4860],{"class":156},[146,5088,3773],{"class":201},[146,5090,435],{"class":156},[146,5092,5093,5095,5098],{"class":148,"line":1148},[146,5094,4912],{"class":156},[146,5096,5097],{"class":163},"\"60\"",[146,5099,435],{"class":156},[146,5101,5102,5104,5107],{"class":148,"line":1154},[146,5103,4869],{"class":156},[146,5105,5106],{"class":163},"\"Optional time-to-live for the bot to run in seconds. if not added the bot will stop after 60s\"",[146,5108,435],{"class":156},[146,5110,5111,5113],{"class":148,"line":1174},[146,5112,4879],{"class":156},[146,5114,5115],{"class":163},"\"60\"\n",[146,5117,5118],{"class":148,"line":1204},[146,5119,3977],{"class":156},[146,5121,5122],{"class":148,"line":1231},[146,5123,4835],{"class":156},[146,5125,5126],{"class":148,"line":1267},[146,5127,189],{"emptyLinePlaceholder":188},[146,5129,5130,5133,5136,5139,5142],{"class":148,"line":1283},[146,5131,5132],{"class":152},"  async",[146,5134,5135],{"class":208}," run",[146,5137,5138],{"class":156},"({ ",[146,5140,5141],{"class":497},"args",[146,5143,5144],{"class":156}," }) {\n",[146,5146,5147,5149],{"class":148,"line":1288},[146,5148,1763],{"class":152},[146,5150,424],{"class":156},[146,5152,5153,5156,5159,5161,5164,5166,5168,5170,5173,5175,5177,5179,5182],{"class":148,"line":1304},[146,5154,5155],{"class":152},"          const",[146,5157,5158],{"class":201}," existingPid",[146,5160,205],{"class":152},[146,5162,5163],{"class":208}," Number",[146,5165,212],{"class":156},[146,5167,2823],{"class":152},[146,5169,1688],{"class":156},[146,5171,5172],{"class":208},"readFile",[146,5174,212],{"class":156},[146,5176,4013],{"class":201},[146,5178,507],{"class":156},[146,5180,5181],{"class":163},"'utf8'",[146,5183,1598],{"class":156},[146,5185,5186,5189,5192,5195,5198],{"class":148,"line":1312},[146,5187,5188],{"class":156},"          process.",[146,5190,5191],{"class":208},"kill",[146,5193,5194],{"class":156},"(existingPid, ",[146,5196,5197],{"class":201},"0",[146,5199,559],{"class":156},[146,5201,5202,5205,5207,5209,5212,5215,5217],{"class":148,"line":1317},[146,5203,5204],{"class":156},"          consola.",[146,5206,2618],{"class":208},[146,5208,212],{"class":156},[146,5210,5211],{"class":163},"`Volume bot already running: ${",[146,5213,5214],{"class":156},"existingPid",[146,5216,753],{"class":163},[146,5218,559],{"class":156},[146,5220,5222,5225],{"class":148,"line":5221},60,[146,5223,5224],{"class":152},"          return",[146,5226,167],{"class":156},[146,5228,5230,5232,5234],{"class":148,"line":5229},61,[146,5231,2127],{"class":156},[146,5233,715],{"class":152},[146,5235,5236],{"class":156}," { }\n",[146,5238,5240],{"class":148,"line":5239},62,[146,5241,615],{"class":156},[146,5243,5245,5247,5249,5252,5255,5257],{"class":148,"line":5244},63,[146,5246,2055],{"class":152},[146,5248,1688],{"class":156},[146,5250,5251],{"class":208},"mkdir",[146,5253,5254],{"class":156},"(dataDir, { recursive: ",[146,5256,250],{"class":201},[146,5258,5259],{"class":156}," });\n",[146,5261,5263,5265,5267,5269,5271,5273,5275,5277,5279],{"class":148,"line":5262},64,[146,5264,1770],{"class":152},[146,5266,3874],{"class":201},[146,5268,205],{"class":152},[146,5270,3879],{"class":208},[146,5272,3882],{"class":156},[146,5274,3885],{"class":208},[146,5276,212],{"class":156},[146,5278,464],{"class":201},[146,5280,3892],{"class":156},[146,5282,5284,5287,5289],{"class":148,"line":5283},65,[146,5285,5286],{"class":156},"            detached: ",[146,5288,250],{"class":201},[146,5290,435],{"class":156},[146,5292,5294,5297,5299],{"class":148,"line":5293},66,[146,5295,5296],{"class":156},"            stdio: ",[146,5298,3909],{"class":163},[146,5300,435],{"class":156},[146,5302,5304],{"class":148,"line":5303},67,[146,5305,5306],{"class":156},"            env: {\n",[146,5308,5310,5313],{"class":148,"line":5309},68,[146,5311,5312],{"class":152},"                ...",[146,5314,3924],{"class":156},[146,5316,5318,5321,5323],{"class":148,"line":5317},69,[146,5319,5320],{"class":156},"                SOLANA_BOTS_INTERNAL: ",[146,5322,3932],{"class":163},[146,5324,435],{"class":156},[146,5326,5328],{"class":148,"line":5327},70,[146,5329,5330],{"class":156},"                TARGET_MINT: args.target,\n",[146,5332,5334],{"class":148,"line":5333},71,[146,5335,5336],{"class":156},"                WALLETS: args.wallets,\n",[146,5338,5340],{"class":148,"line":5339},72,[146,5341,5342],{"class":156},"                AMOUNT: args.amount,\n",[146,5344,5346],{"class":148,"line":5345},73,[146,5347,5348],{"class":156},"                PASSWORD: args.password,\n",[146,5350,5352],{"class":148,"line":5351},74,[146,5353,5354],{"class":156},"                INTERVAL: args.interval,\n",[146,5356,5358,5361,5363,5365],{"class":148,"line":5357},75,[146,5359,5360],{"class":156},"                TTL: args.ttl ",[146,5362,3967],{"class":152},[146,5364,3970],{"class":163},[146,5366,435],{"class":156},[146,5368,5370],{"class":148,"line":5369},76,[146,5371,5372],{"class":156},"            },\n",[146,5374,5376],{"class":148,"line":5375},77,[146,5377,2092],{"class":156},[146,5379,5381],{"class":148,"line":5380},78,[146,5382,189],{"emptyLinePlaceholder":188},[146,5384,5386,5389,5391],{"class":148,"line":5385},79,[146,5387,5388],{"class":156},"        child.",[146,5390,3993],{"class":208},[146,5392,688],{"class":156},[146,5394,5396],{"class":148,"line":5395},80,[146,5397,189],{"emptyLinePlaceholder":188},[146,5399,5401,5403,5405,5407,5409,5411,5413,5415],{"class":148,"line":5400},81,[146,5402,2055],{"class":152},[146,5404,1688],{"class":156},[146,5406,4008],{"class":208},[146,5408,212],{"class":156},[146,5410,4013],{"class":201},[146,5412,507],{"class":156},[146,5414,2920],{"class":208},[146,5416,4020],{"class":156},[146,5418,5420,5422,5424,5426,5428,5430,5432,5434,5436],{"class":148,"line":5419},82,[146,5421,1620],{"class":156},[146,5423,1488],{"class":208},[146,5425,212],{"class":156},[146,5427,4032],{"class":163},[146,5429,4035],{"class":156},[146,5431,33],{"class":163},[146,5433,4040],{"class":156},[146,5435,753],{"class":163},[146,5437,559],{"class":156},[146,5439,5441],{"class":148,"line":5440},83,[146,5442,4835],{"class":156},[146,5444,5446],{"class":148,"line":5445},84,[146,5447,5448],{"class":156},"  \n",[146,5450,5452],{"class":148,"line":5451},85,[146,5453,358],{"class":156},[10,5455,5456,5457,5459,5460,501],{},"You then simply import all your commands into a single ",[143,5458,3833],{}," file, and call ",[143,5461,5462],{},"runMain(main)",[135,5464,5466],{"className":137,"code":5465,"filename":3833,"language":140,"meta":141,"style":141},"if (process.env['SOLANA_BOTS_INTERNAL'] === 'volume-worker') {\n  try {\n    await volumeBot(\n      process.env['TARGET_MINT']!,\n      Number(process.env['AMOUNT']),\n      Number(process.env['WALLETS']),\n      process.env['PASSWORD']!,\n      Number(process.env['INTERVAL']),\n      (process.env['MASS_OUT'] === 'true' ? true : false),\n      process.env['TTL'] ? Number(process.env['TTL']) : undefined,\n    );\n  } catch (err) {\n    consola.error(err);\n    process.exitCode = 1;\n  }\n  process.exit(0);\n}\n\nconst start = defineCommand({\n  meta: {\n    name: 'Solana Bots',\n    description: 'Start the wizard',\n  },\n  subCommands: {\n    main: storeMainWallet,\n    create: createWallets,\n    'api-rpc': apiAndRpc,\n    drain: drainWallets,\n    split: splitFounds,\n    'read-all': readAllWallets,\n    read: readWalletView,\n    'volume-start': volumeStart,\n    'volume-stop': volumeStop,\n    'volume-status': volumeStatus,\n    airdrop: getAirDropC,\n    buy,\n    sell,\n    out: massOut\n  },\n\n  async run({args}) {\n   if (args._ && args._.length > 0) {\n        return; \n    }\n\n     consola.box('Solana Bots - Starter');\n     // you got the idea\n  }\n})\nawait runMain(start);\n",[143,5467,5468,5487,5493,5502,5517,5530,5541,5554,5565,5591,5618,5623,5631,5639,5650,5654,5668,5672,5676,5689,5693,5702,5711,5715,5720,5725,5730,5738,5743,5748,5756,5761,5769,5777,5785,5790,5795,5800,5805,5809,5813,5827,5848,5854,5858,5862,5877,5882,5886,5891],{"__ignoreMap":141},[146,5469,5470,5472,5475,5478,5480,5482,5485],{"class":148,"line":149},[146,5471,3439],{"class":152},[146,5473,5474],{"class":156}," (process.env[",[146,5476,5477],{"class":163},"'SOLANA_BOTS_INTERNAL'",[146,5479,1566],{"class":156},[146,5481,1239],{"class":152},[146,5483,5484],{"class":163}," 'volume-worker'",[146,5486,1171],{"class":156},[146,5488,5489,5491],{"class":148,"line":170},[146,5490,533],{"class":152},[146,5492,424],{"class":156},[146,5494,5495,5497,5500],{"class":148,"line":185},[146,5496,4441],{"class":152},[146,5498,5499],{"class":208}," volumeBot",[146,5501,2857],{"class":156},[146,5503,5504,5507,5510,5513,5515],{"class":148,"line":192},[146,5505,5506],{"class":156},"      process.env[",[146,5508,5509],{"class":163},"'TARGET_MINT'",[146,5511,5512],{"class":156},"]",[146,5514,1712],{"class":152},[146,5516,435],{"class":156},[146,5518,5519,5522,5524,5527],{"class":148,"line":221},[146,5520,5521],{"class":208},"      Number",[146,5523,4703],{"class":156},[146,5525,5526],{"class":163},"'AMOUNT'",[146,5528,5529],{"class":156},"]),\n",[146,5531,5532,5534,5536,5539],{"class":148,"line":256},[146,5533,5521],{"class":208},[146,5535,4703],{"class":156},[146,5537,5538],{"class":163},"'WALLETS'",[146,5540,5529],{"class":156},[146,5542,5543,5545,5548,5550,5552],{"class":148,"line":279},[146,5544,5506],{"class":156},[146,5546,5547],{"class":163},"'PASSWORD'",[146,5549,5512],{"class":156},[146,5551,1712],{"class":152},[146,5553,435],{"class":156},[146,5555,5556,5558,5560,5563],{"class":148,"line":300},[146,5557,5521],{"class":208},[146,5559,4703],{"class":156},[146,5561,5562],{"class":163},"'INTERVAL'",[146,5564,5529],{"class":156},[146,5566,5567,5570,5573,5575,5577,5580,5582,5584,5586,5589],{"class":148,"line":321},[146,5568,5569],{"class":156},"      (process.env[",[146,5571,5572],{"class":163},"'MASS_OUT'",[146,5574,1566],{"class":156},[146,5576,1239],{"class":152},[146,5578,5579],{"class":163}," 'true'",[146,5581,1192],{"class":152},[146,5583,4522],{"class":201},[146,5585,3674],{"class":152},[146,5587,5588],{"class":201}," false",[146,5590,352],{"class":156},[146,5592,5593,5595,5598,5600,5602,5604,5606,5608,5611,5613,5616],{"class":148,"line":355},[146,5594,5506],{"class":156},[146,5596,5597],{"class":163},"'TTL'",[146,5599,1566],{"class":156},[146,5601,3667],{"class":152},[146,5603,5163],{"class":208},[146,5605,4703],{"class":156},[146,5607,5597],{"class":163},[146,5609,5610],{"class":156},"]) ",[146,5612,501],{"class":152},[146,5614,5615],{"class":201}," undefined",[146,5617,435],{"class":156},[146,5619,5620],{"class":148,"line":562},[146,5621,5622],{"class":156},"    );\n",[146,5624,5625,5627,5629],{"class":148,"line":584},[146,5626,712],{"class":156},[146,5628,715],{"class":152},[146,5630,718],{"class":156},[146,5632,5633,5635,5637],{"class":148,"line":612},[146,5634,1291],{"class":156},[146,5636,1163],{"class":208},[146,5638,2153],{"class":156},[146,5640,5641,5644,5646,5648],{"class":148,"line":618},[146,5642,5643],{"class":156},"    process.exitCode ",[146,5645,955],{"class":152},[146,5647,2696],{"class":201},[146,5649,167],{"class":156},[146,5651,5652],{"class":148,"line":641},[146,5653,761],{"class":156},[146,5655,5656,5659,5662,5664,5666],{"class":148,"line":672},[146,5657,5658],{"class":156},"  process.",[146,5660,5661],{"class":208},"exit",[146,5663,212],{"class":156},[146,5665,5197],{"class":201},[146,5667,559],{"class":156},[146,5669,5670],{"class":148,"line":691},[146,5671,1507],{"class":156},[146,5673,5674],{"class":148,"line":696},[146,5675,189],{"emptyLinePlaceholder":188},[146,5677,5678,5680,5683,5685,5687],{"class":148,"line":709},[146,5679,416],{"class":152},[146,5681,5682],{"class":201}," start",[146,5684,205],{"class":152},[146,5686,4803],{"class":208},[146,5688,1449],{"class":156},[146,5690,5691],{"class":148,"line":721},[146,5692,4810],{"class":156},[146,5694,5695,5697,5700],{"class":148,"line":758},[146,5696,4815],{"class":156},[146,5698,5699],{"class":163},"'Solana Bots'",[146,5701,435],{"class":156},[146,5703,5704,5706,5709],{"class":148,"line":764},[146,5705,4825],{"class":156},[146,5707,5708],{"class":163},"'Start the wizard'",[146,5710,435],{"class":156},[146,5712,5713],{"class":148,"line":769},[146,5714,4835],{"class":156},[146,5716,5717],{"class":148,"line":774},[146,5718,5719],{"class":156},"  subCommands: {\n",[146,5721,5722],{"class":148,"line":813},[146,5723,5724],{"class":156},"    main: storeMainWallet,\n",[146,5726,5727],{"class":148,"line":820},[146,5728,5729],{"class":156},"    create: createWallets,\n",[146,5731,5732,5735],{"class":148,"line":825},[146,5733,5734],{"class":163},"    'api-rpc'",[146,5736,5737],{"class":156},": apiAndRpc,\n",[146,5739,5740],{"class":148,"line":840},[146,5741,5742],{"class":156},"    drain: drainWallets,\n",[146,5744,5745],{"class":148,"line":856},[146,5746,5747],{"class":156},"    split: splitFounds,\n",[146,5749,5750,5753],{"class":148,"line":871},[146,5751,5752],{"class":163},"    'read-all'",[146,5754,5755],{"class":156},": readAllWallets,\n",[146,5757,5758],{"class":148,"line":895},[146,5759,5760],{"class":156},"    read: readWalletView,\n",[146,5762,5763,5766],{"class":148,"line":900},[146,5764,5765],{"class":163},"    'volume-start'",[146,5767,5768],{"class":156},": volumeStart,\n",[146,5770,5771,5774],{"class":148,"line":918},[146,5772,5773],{"class":163},"    'volume-stop'",[146,5775,5776],{"class":156},": volumeStop,\n",[146,5778,5779,5782],{"class":148,"line":935},[146,5780,5781],{"class":163},"    'volume-status'",[146,5783,5784],{"class":156},": volumeStatus,\n",[146,5786,5787],{"class":148,"line":941},[146,5788,5789],{"class":156},"    airdrop: getAirDropC,\n",[146,5791,5792],{"class":148,"line":946},[146,5793,5794],{"class":156},"    buy,\n",[146,5796,5797],{"class":148,"line":963},[146,5798,5799],{"class":156},"    sell,\n",[146,5801,5802],{"class":148,"line":987},[146,5803,5804],{"class":156},"    out: massOut\n",[146,5806,5807],{"class":148,"line":1007},[146,5808,4835],{"class":156},[146,5810,5811],{"class":148,"line":1027},[146,5812,189],{"emptyLinePlaceholder":188},[146,5814,5815,5817,5819,5822,5824],{"class":148,"line":1044},[146,5816,5132],{"class":152},[146,5818,5135],{"class":208},[146,5820,5821],{"class":156},"({",[146,5823,5141],{"class":497},[146,5825,5826],{"class":156},"}) {\n",[146,5828,5829,5832,5835,5837,5840,5842,5844,5846],{"class":148,"line":1049},[146,5830,5831],{"class":152},"   if",[146,5833,5834],{"class":156}," (args._ ",[146,5836,1861],{"class":152},[146,5838,5839],{"class":156}," args._.",[146,5841,909],{"class":201},[146,5843,3660],{"class":152},[146,5845,958],{"class":201},[146,5847,1171],{"class":156},[146,5849,5850,5852],{"class":148,"line":1072},[146,5851,2120],{"class":152},[146,5853,853],{"class":156},[146,5855,5856],{"class":148,"line":1093},[146,5857,938],{"class":156},[146,5859,5860],{"class":148,"line":1105},[146,5861,189],{"emptyLinePlaceholder":188},[146,5863,5864,5867,5870,5872,5875],{"class":148,"line":1110},[146,5865,5866],{"class":156},"     consola.",[146,5868,5869],{"class":208},"box",[146,5871,212],{"class":156},[146,5873,5874],{"class":163},"'Solana Bots - Starter'",[146,5876,559],{"class":156},[146,5878,5879],{"class":148,"line":1127},[146,5880,5881],{"class":2896},"     // you got the idea\n",[146,5883,5884],{"class":148,"line":1138},[146,5885,761],{"class":156},[146,5887,5888],{"class":148,"line":1148},[146,5889,5890],{"class":156},"})\n",[146,5892,5893,5895,5898],{"class":148,"line":1154},[146,5894,2823],{"class":152},[146,5896,5897],{"class":208}," runMain",[146,5899,5900],{"class":156},"(start);\n",[10,5902,5903,5906],{},[21,5904,132],{"href":130,"rel":5905},[25]," automatically generates a help menu for you, for every single command.",[10,5908,5909,5910,5915,5916,5919],{},"I was then configuring ",[21,5911,5914],{"href":5912,"rel":5913},"https://github.com/vercel/pkg",[25],"pkg"," which was a pain in the ass (I didn't notice it was archived), it doesn't support esm, newer versions of node, and the list goes on, so I ditched it, and used ",[143,5917,5918],{},"bun"," instead, that resulted in a faster build, and cli overall. That also means I needed to switch to bun's built in sqlite instead of better-sqlite3.",[10,5921,5922],{},"My friend tried it. As expected, managing multiple configuration flags, reading JSON logs, and interacting via terminal commands was a bit too much for a non-technical user. It was time to wrap all of this into a nice, friendly desktop app using Electron.",[35,5924,5926],{"id":5925},"the-electron-experience","The Electron Experience",[10,5928,5929],{},"While Electron is extremely powerful, and really an awesome framework (I was really impressed by it), its complexity comes from the boilerplate it requires you to wire, and its build tools.",[10,5931,5932],{},"Setting up the dev environment is pretty easy, you run:",[135,5934,5938],{"className":5935,"code":5936,"language":5937,"meta":141,"style":141},"language-bash shiki shiki-themes github-light github-dark github-dark","npx create-electron-app@latest my-new-app --template=vite-typescript\n","bash",[143,5939,5940],{"__ignoreMap":141},[146,5941,5942,5945,5948,5951],{"class":148,"line":149},[146,5943,5944],{"class":208},"npx",[146,5946,5947],{"class":163}," create-electron-app@latest",[146,5949,5950],{"class":163}," my-new-app",[146,5952,5953],{"class":201}," --template=vite-typescript\n",[10,5955,5956,5957,5962,5963,33],{},"That creates the Vite setup using electron forge ",[21,5958,5961],{"href":5959,"rel":5960},"https://www.electronforge.io/config/plugins/vite#native-node-modules",[25],"template",", and since I used Vue, for this app I followed the ",[21,5964,5967],{"href":5965,"rel":5966},"https://www.electronforge.io/guides/framework-integration/vue-3",[25],"Vue integration guide",[10,5969,5970,5971,5976,5977,1514,5982,5987],{},"After exploring and reading the forge docs a bit, I realized that the build process should end up pretty simple, so I continued with the frontend development without thinking much about it. I used ",[21,5972,5975],{"href":5973,"rel":5974},"https://ui.nuxt.com/",[25],"Nuxt UI"," ",[21,5978,5981],{"href":5979,"rel":5980},"https://tailwindcss.com/",[25],"Tailwind",[21,5983,5986],{"href":5984,"rel":5985},"https://router.vuejs.org/",[25],"Vue Router",", so I was able to move fast and complete the frontend pretty quickly.",[10,5989,5990,5991,5996,5997,6000,6001,1514,6004,6009,6010,33],{},"I then started to test it to see how the backend of the system interacts with the frontend. Discovered ",[21,5992,5995],{"href":5993,"rel":5994},"https://www.electronjs.org/docs/latest/tutorial/ipc",[25],"Electron's IPC"," model that you should provide your ",[143,5998,5999],{},"renderer.ts"," explicitly the methods you want it to have from the ",[143,6002,6003],{},"preload.ts",[21,6005,6008],{"href":6006,"rel":6007},"https://www.electronjs.org/docs/latest/tutorial/context-isolation",[25],"context isolation",". And communication between the frontend and the main process or vice versa, only via ",[143,6011,6003],{},[10,6013,6014,6015,6017,6018,33],{},"So that is what I did. I wired everything up so that the components and forms from the renderer call the methods defined in ",[143,6016,6003],{},", which in turn trigger the main process via ",[143,6019,6020],{},"ipcRenderer.invoke()",[10,6022,6023],{},"However, for the Volume Bot, the approach had to be a bit different.",[10,6025,6026,6027,6029,6030,33],{},"Because the volume bot runs a continuous, heavy background loop dealing with cryptography, network requests, and database writes, running it directly inside Electron's main process would completely choke up the application. If the main process blocks, the entire UI freezes. In the CLI version, I used Node's ",[143,6028,3828],{},", but Electron provides a dedicated API specifically designed for running heavy, Node.js-dependent background tasks: the ",[21,6031,6034],{"href":6032,"rel":6033},"https://www.electronjs.org/docs/latest/api/utility-process",[25],[143,6035,6036],{},"utilityProcess",[10,6038,6039],{},"To bridge the gap between this detached utility process and the Vue frontend, I needed a way to stream real-time stats and logs so the user could see what the bot was doing.",[10,6041,6042,6043,270],{},"Instead of setting up complex message ports, I used a simpler approach: standard output stream parsing. Inside the worker script, whenever the bot updates its stats, it simply logs a stringified JSON object prefixed with a special token (",[143,6044,6045],{},"__BOT_METRIC__:",[10,6047,6048],{},"Here is how I implemented the bridge in the main Electron process:",[135,6050,6053],{"className":137,"code":6051,"filename":6052,"language":140,"meta":141,"style":141},"\nimport { ipcMain, safeStorage, utilityProcess } from 'electron';\nimport readline from 'node:readline';\n// ...\n\nexport function setupVolumeBotBridge(mainWindow: BrowserWindow, getVault: () => string | null) {\n  \n ipcMain.handle('trade:volume-bot', async (_event, targetMintAddress, maxSolPerWallet, numberOfWallets, interval, shouldMassOut, ttl) => {\n    const vault = getVault();\n    if (!vault) return { ok: false, reason: 'UNAUTHORIZED'};\n    const decryptedPassword = safeStorage.decryptString(Buffer.from(vault, 'base64'));  \n\n    //fork heavy worker\n    workerThread = utilityProcess.fork(cliScriptPath, [], {\n        env: {\n          ...process.env,\n          SOLANA_BOTS_INTERNAL: 'volume-worker',\n          TARGET_MINT: targetMintAddress,\n          AMOUNT: String(maxSolPerWallet),\n          WALLETS: String(numberOfWallets),\n          PASSWORD: decryptedPassword,\n          INTERVAL: String(interval ?? 0),\n          MASS_OUT: String(shouldMassOut),\n          TTL: ttl ? String(ttl) : \"\"\n        },\n        stdio: 'pipe'\n    });\n      \n    if (workerThread.stdout) {\n        // read the output\n        const stdoutReader = readline.createInterface({ input: workerThread.stdout });\n        \n        stdoutReader.on('line', (line) => {\n          const cleanLine = line.trim();\n          if (!cleanLine) return;\n\n          // metric token\n          if (cleanLine.startsWith('__BOT_METRIC__:')) {\n            try {\n              const event = JSON.parse(cleanLine.replace('__BOT_METRIC__:', ''));\n              \n              // Route the data to the frontend\n              switch (event.type) {\n                case 'stats':\n                  mainWindow.webContents.send('volume-bot:stats', event.payload);\n                  break;\n                case 'log':\n                  mainWindow.webContents.send('volume-bot:log', event.payload);\n                  break;\n                case 'activity':\n                  mainWindow.webContents.send('volume-bot:activity', event.payload);\n                  break;\n                case 'status':\n                  mainWindow.webContents.send('volume-bot:status', event.payload);\n                  break;\n              }\n            } catch (err) {\n              console.error('Failed to parse bot metric line:', err);\n            }\n          } else {\n             // pass standard console logs directly to the UI log viewer\n            mainWindow.webContents.send('volume-bot:raw-console', cleanLine);\n          }\n        });\n      }\n\n      workerThread.on('exit', () => {\n        mainWindow.webContents.send('volume-bot:status', { status: 'stopped', stoppedAt: Date.now() });\n        workerThread = null;\n      });\n\n      return { ok: true, data: 'SUCCESS'};\n  });\n  \n  // stop logic\n}\n","volumeBotIPC.ts",[143,6054,6055,6059,6073,6087,6092,6096,6136,6140,6196,6210,6237,6265,6269,6274,6290,6295,6302,6312,6317,6327,6337,6342,6357,6367,6385,6390,6398,6402,6407,6414,6419,6437,6442,6465,6481,6497,6501,6506,6523,6530,6562,6567,6572,6580,6591,6607,6614,6623,6636,6642,6651,6664,6670,6679,6692,6698,6703,6711,6725,6729,6738,6743,6758,6763,6767,6772,6776,6794,6819,6830,6835,6839,6855,6860,6864,6869],{"__ignoreMap":141},[146,6056,6057],{"class":148,"line":149},[146,6058,189],{"emptyLinePlaceholder":188},[146,6060,6061,6063,6066,6068,6071],{"class":148,"line":170},[146,6062,153],{"class":152},[146,6064,6065],{"class":156}," { ipcMain, safeStorage, utilityProcess } ",[146,6067,160],{"class":152},[146,6069,6070],{"class":163}," 'electron'",[146,6072,167],{"class":156},[146,6074,6075,6077,6080,6082,6085],{"class":148,"line":185},[146,6076,153],{"class":152},[146,6078,6079],{"class":156}," readline ",[146,6081,160],{"class":152},[146,6083,6084],{"class":163}," 'node:readline'",[146,6086,167],{"class":156},[146,6088,6089],{"class":148,"line":192},[146,6090,6091],{"class":2896},"// ...\n",[146,6093,6094],{"class":148,"line":221},[146,6095,189],{"emptyLinePlaceholder":188},[146,6097,6098,6100,6102,6105,6107,6110,6112,6115,6117,6120,6122,6125,6127,6129,6132,6134],{"class":148,"line":256},[146,6099,195],{"class":152},[146,6101,1381],{"class":152},[146,6103,6104],{"class":208}," setupVolumeBotBridge",[146,6106,212],{"class":156},[146,6108,6109],{"class":497},"mainWindow",[146,6111,501],{"class":152},[146,6113,6114],{"class":208}," BrowserWindow",[146,6116,507],{"class":156},[146,6118,6119],{"class":208},"getVault",[146,6121,501],{"class":152},[146,6123,6124],{"class":156}," () ",[146,6126,1879],{"class":152},[146,6128,515],{"class":201},[146,6130,6131],{"class":152}," |",[146,6133,1199],{"class":201},[146,6135,1171],{"class":156},[146,6137,6138],{"class":148,"line":279},[146,6139,5448],{"class":156},[146,6141,6142,6145,6148,6150,6153,6155,6157,6159,6162,6164,6166,6168,6171,6173,6175,6177,6180,6182,6185,6187,6190,6192,6194],{"class":148,"line":300},[146,6143,6144],{"class":156}," ipcMain.",[146,6146,6147],{"class":208},"handle",[146,6149,212],{"class":156},[146,6151,6152],{"class":163},"'trade:volume-bot'",[146,6154,507],{"class":156},[146,6156,2832],{"class":152},[146,6158,494],{"class":156},[146,6160,6161],{"class":497},"_event",[146,6163,507],{"class":156},[146,6165,3754],{"class":497},[146,6167,507],{"class":156},[146,6169,6170],{"class":497},"maxSolPerWallet",[146,6172,507],{"class":156},[146,6174,4188],{"class":497},[146,6176,507],{"class":156},[146,6178,6179],{"class":497},"interval",[146,6181,507],{"class":156},[146,6183,6184],{"class":497},"shouldMassOut",[146,6186,507],{"class":156},[146,6188,6189],{"class":497},"ttl",[146,6191,1876],{"class":156},[146,6193,1879],{"class":152},[146,6195,424],{"class":156},[146,6197,6198,6200,6203,6205,6208],{"class":148,"line":321},[146,6199,540],{"class":152},[146,6201,6202],{"class":201}," vault",[146,6204,205],{"class":152},[146,6206,6207],{"class":208}," getVault",[146,6209,688],{"class":156},[146,6211,6212,6214,6216,6218,6221,6224,6227,6229,6232,6235],{"class":148,"line":355},[146,6213,903],{"class":152},[146,6215,494],{"class":156},[146,6217,1712],{"class":152},[146,6219,6220],{"class":156},"vault) ",[146,6222,6223],{"class":152},"return",[146,6225,6226],{"class":156}," { ok: ",[146,6228,3773],{"class":201},[146,6230,6231],{"class":156},", reason: ",[146,6233,6234],{"class":163},"'UNAUTHORIZED'",[146,6236,472],{"class":156},[146,6238,6239,6241,6244,6246,6249,6252,6254,6256,6259,6262],{"class":148,"line":562},[146,6240,540],{"class":152},[146,6242,6243],{"class":201}," decryptedPassword",[146,6245,205],{"class":152},[146,6247,6248],{"class":156}," safeStorage.",[146,6250,6251],{"class":208},"decryptString",[146,6253,1119],{"class":156},[146,6255,160],{"class":208},[146,6257,6258],{"class":156},"(vault, ",[146,6260,6261],{"class":163},"'base64'",[146,6263,6264],{"class":156},"));  \n",[146,6266,6267],{"class":148,"line":584},[146,6268,189],{"emptyLinePlaceholder":188},[146,6270,6271],{"class":148,"line":612},[146,6272,6273],{"class":2896},"    //fork heavy worker\n",[146,6275,6276,6279,6281,6284,6287],{"class":148,"line":618},[146,6277,6278],{"class":156},"    workerThread ",[146,6280,955],{"class":152},[146,6282,6283],{"class":156}," utilityProcess.",[146,6285,6286],{"class":208},"fork",[146,6288,6289],{"class":156},"(cliScriptPath, [], {\n",[146,6291,6292],{"class":148,"line":641},[146,6293,6294],{"class":156},"        env: {\n",[146,6296,6297,6300],{"class":148,"line":672},[146,6298,6299],{"class":152},"          ...",[146,6301,3924],{"class":156},[146,6303,6304,6307,6310],{"class":148,"line":691},[146,6305,6306],{"class":156},"          SOLANA_BOTS_INTERNAL: ",[146,6308,6309],{"class":163},"'volume-worker'",[146,6311,435],{"class":156},[146,6313,6314],{"class":148,"line":696},[146,6315,6316],{"class":156},"          TARGET_MINT: targetMintAddress,\n",[146,6318,6319,6322,6324],{"class":148,"line":709},[146,6320,6321],{"class":156},"          AMOUNT: ",[146,6323,2920],{"class":208},[146,6325,6326],{"class":156},"(maxSolPerWallet),\n",[146,6328,6329,6332,6334],{"class":148,"line":721},[146,6330,6331],{"class":156},"          WALLETS: ",[146,6333,2920],{"class":208},[146,6335,6336],{"class":156},"(numberOfWallets),\n",[146,6338,6339],{"class":148,"line":758},[146,6340,6341],{"class":156},"          PASSWORD: decryptedPassword,\n",[146,6343,6344,6347,6349,6351,6353,6355],{"class":148,"line":764},[146,6345,6346],{"class":156},"          INTERVAL: ",[146,6348,2920],{"class":208},[146,6350,4446],{"class":156},[146,6352,3967],{"class":152},[146,6354,958],{"class":201},[146,6356,352],{"class":156},[146,6358,6359,6362,6364],{"class":148,"line":769},[146,6360,6361],{"class":156},"          MASS_OUT: ",[146,6363,2920],{"class":208},[146,6365,6366],{"class":156},"(shouldMassOut),\n",[146,6368,6369,6372,6374,6377,6380,6382],{"class":148,"line":774},[146,6370,6371],{"class":156},"          TTL: ttl ",[146,6373,3667],{"class":152},[146,6375,6376],{"class":208}," String",[146,6378,6379],{"class":156},"(ttl) ",[146,6381,501],{"class":152},[146,6383,6384],{"class":163}," \"\"\n",[146,6386,6387],{"class":148,"line":813},[146,6388,6389],{"class":156},"        },\n",[146,6391,6392,6395],{"class":148,"line":820},[146,6393,6394],{"class":156},"        stdio: ",[146,6396,6397],{"class":163},"'pipe'\n",[146,6399,6400],{"class":148,"line":825},[146,6401,1477],{"class":156},[146,6403,6404],{"class":148,"line":840},[146,6405,6406],{"class":156},"      \n",[146,6408,6409,6411],{"class":148,"line":856},[146,6410,903],{"class":152},[146,6412,6413],{"class":156}," (workerThread.stdout) {\n",[146,6415,6416],{"class":148,"line":871},[146,6417,6418],{"class":2896},"        // read the output\n",[146,6420,6421,6423,6426,6428,6431,6434],{"class":148,"line":895},[146,6422,1770],{"class":152},[146,6424,6425],{"class":201}," stdoutReader",[146,6427,205],{"class":152},[146,6429,6430],{"class":156}," readline.",[146,6432,6433],{"class":208},"createInterface",[146,6435,6436],{"class":156},"({ input: workerThread.stdout });\n",[146,6438,6439],{"class":148,"line":900},[146,6440,6441],{"class":156},"        \n",[146,6443,6444,6447,6449,6451,6454,6457,6459,6461,6463],{"class":148,"line":918},[146,6445,6446],{"class":156},"        stdoutReader.",[146,6448,4500],{"class":208},[146,6450,212],{"class":156},[146,6452,6453],{"class":163},"'line'",[146,6455,6456],{"class":156},", (",[146,6458,148],{"class":497},[146,6460,1876],{"class":156},[146,6462,1879],{"class":152},[146,6464,424],{"class":156},[146,6466,6467,6469,6472,6474,6477,6479],{"class":148,"line":935},[146,6468,5155],{"class":152},[146,6470,6471],{"class":201}," cleanLine",[146,6473,205],{"class":152},[146,6475,6476],{"class":156}," line.",[146,6478,1781],{"class":208},[146,6480,688],{"class":156},[146,6482,6483,6486,6488,6490,6493,6495],{"class":148,"line":941},[146,6484,6485],{"class":152},"          if",[146,6487,494],{"class":156},[146,6489,1712],{"class":152},[146,6491,6492],{"class":156},"cleanLine) ",[146,6494,6223],{"class":152},[146,6496,167],{"class":156},[146,6498,6499],{"class":148,"line":946},[146,6500,189],{"emptyLinePlaceholder":188},[146,6502,6503],{"class":148,"line":963},[146,6504,6505],{"class":2896},"          // metric token\n",[146,6507,6508,6510,6513,6516,6518,6521],{"class":148,"line":987},[146,6509,6485],{"class":152},[146,6511,6512],{"class":156}," (cleanLine.",[146,6514,6515],{"class":208},"startsWith",[146,6517,212],{"class":156},[146,6519,6520],{"class":163},"'__BOT_METRIC__:'",[146,6522,1264],{"class":156},[146,6524,6525,6528],{"class":148,"line":1007},[146,6526,6527],{"class":152},"            try",[146,6529,424],{"class":156},[146,6531,6532,6534,6537,6539,6541,6543,6545,6548,6551,6553,6555,6557,6560],{"class":148,"line":1027},[146,6533,2981],{"class":152},[146,6535,6536],{"class":201}," event",[146,6538,205],{"class":152},[146,6540,1821],{"class":201},[146,6542,33],{"class":156},[146,6544,1826],{"class":208},[146,6546,6547],{"class":156},"(cleanLine.",[146,6549,6550],{"class":208},"replace",[146,6552,212],{"class":156},[146,6554,6520],{"class":163},[146,6556,507],{"class":156},[146,6558,6559],{"class":163},"''",[146,6561,1598],{"class":156},[146,6563,6564],{"class":148,"line":1044},[146,6565,6566],{"class":156},"              \n",[146,6568,6569],{"class":148,"line":1049},[146,6570,6571],{"class":2896},"              // Route the data to the frontend\n",[146,6573,6574,6577],{"class":148,"line":1072},[146,6575,6576],{"class":152},"              switch",[146,6578,6579],{"class":156}," (event.type) {\n",[146,6581,6582,6585,6588],{"class":148,"line":1093},[146,6583,6584],{"class":152},"                case",[146,6586,6587],{"class":163}," 'stats'",[146,6589,6590],{"class":156},":\n",[146,6592,6593,6596,6599,6601,6604],{"class":148,"line":1105},[146,6594,6595],{"class":156},"                  mainWindow.webContents.",[146,6597,6598],{"class":208},"send",[146,6600,212],{"class":156},[146,6602,6603],{"class":163},"'volume-bot:stats'",[146,6605,6606],{"class":156},", event.payload);\n",[146,6608,6609,6612],{"class":148,"line":1110},[146,6610,6611],{"class":152},"                  break",[146,6613,167],{"class":156},[146,6615,6616,6618,6621],{"class":148,"line":1127},[146,6617,6584],{"class":152},[146,6619,6620],{"class":163}," 'log'",[146,6622,6590],{"class":156},[146,6624,6625,6627,6629,6631,6634],{"class":148,"line":1138},[146,6626,6595],{"class":156},[146,6628,6598],{"class":208},[146,6630,212],{"class":156},[146,6632,6633],{"class":163},"'volume-bot:log'",[146,6635,6606],{"class":156},[146,6637,6638,6640],{"class":148,"line":1148},[146,6639,6611],{"class":152},[146,6641,167],{"class":156},[146,6643,6644,6646,6649],{"class":148,"line":1154},[146,6645,6584],{"class":152},[146,6647,6648],{"class":163}," 'activity'",[146,6650,6590],{"class":156},[146,6652,6653,6655,6657,6659,6662],{"class":148,"line":1174},[146,6654,6595],{"class":156},[146,6656,6598],{"class":208},[146,6658,212],{"class":156},[146,6660,6661],{"class":163},"'volume-bot:activity'",[146,6663,6606],{"class":156},[146,6665,6666,6668],{"class":148,"line":1204},[146,6667,6611],{"class":152},[146,6669,167],{"class":156},[146,6671,6672,6674,6677],{"class":148,"line":1231},[146,6673,6584],{"class":152},[146,6675,6676],{"class":163}," 'status'",[146,6678,6590],{"class":156},[146,6680,6681,6683,6685,6687,6690],{"class":148,"line":1267},[146,6682,6595],{"class":156},[146,6684,6598],{"class":208},[146,6686,212],{"class":156},[146,6688,6689],{"class":163},"'volume-bot:status'",[146,6691,6606],{"class":156},[146,6693,6694,6696],{"class":148,"line":1283},[146,6695,6611],{"class":152},[146,6697,167],{"class":156},[146,6699,6700],{"class":148,"line":1288},[146,6701,6702],{"class":156},"              }\n",[146,6704,6705,6707,6709],{"class":148,"line":1304},[146,6706,1916],{"class":156},[146,6708,715],{"class":152},[146,6710,718],{"class":156},[146,6712,6713,6716,6718,6720,6723],{"class":148,"line":1312},[146,6714,6715],{"class":156},"              console.",[146,6717,1163],{"class":208},[146,6719,212],{"class":156},[146,6721,6722],{"class":163},"'Failed to parse bot metric line:'",[146,6724,3565],{"class":156},[146,6726,6727],{"class":148,"line":1317},[146,6728,1980],{"class":156},[146,6730,6731,6734,6736],{"class":148,"line":5221},[146,6732,6733],{"class":156},"          } ",[146,6735,1919],{"class":152},[146,6737,424],{"class":156},[146,6739,6740],{"class":148,"line":5229},[146,6741,6742],{"class":2896},"             // pass standard console logs directly to the UI log viewer\n",[146,6744,6745,6748,6750,6752,6755],{"class":148,"line":5239},[146,6746,6747],{"class":156},"            mainWindow.webContents.",[146,6749,6598],{"class":208},[146,6751,212],{"class":156},[146,6753,6754],{"class":163},"'volume-bot:raw-console'",[146,6756,6757],{"class":156},", cleanLine);\n",[146,6759,6760],{"class":148,"line":5244},[146,6761,6762],{"class":156},"          }\n",[146,6764,6765],{"class":148,"line":5262},[146,6766,2092],{"class":156},[146,6768,6769],{"class":148,"line":5283},[146,6770,6771],{"class":156},"      }\n",[146,6773,6774],{"class":148,"line":5293},[146,6775,189],{"emptyLinePlaceholder":188},[146,6777,6778,6781,6783,6785,6788,6790,6792],{"class":148,"line":5303},[146,6779,6780],{"class":156},"      workerThread.",[146,6782,4500],{"class":208},[146,6784,212],{"class":156},[146,6786,6787],{"class":163},"'exit'",[146,6789,4508],{"class":156},[146,6791,1879],{"class":152},[146,6793,424],{"class":156},[146,6795,6796,6799,6801,6803,6805,6808,6811,6814,6816],{"class":148,"line":5309},[146,6797,6798],{"class":156},"        mainWindow.webContents.",[146,6800,6598],{"class":208},[146,6802,212],{"class":156},[146,6804,6689],{"class":163},[146,6806,6807],{"class":156},", { status: ",[146,6809,6810],{"class":163},"'stopped'",[146,6812,6813],{"class":156},", stoppedAt: Date.",[146,6815,4109],{"class":208},[146,6817,6818],{"class":156},"() });\n",[146,6820,6821,6824,6826,6828],{"class":148,"line":5317},[146,6822,6823],{"class":156},"        workerThread ",[146,6825,955],{"class":152},[146,6827,1199],{"class":201},[146,6829,167],{"class":156},[146,6831,6832],{"class":148,"line":5327},[146,6833,6834],{"class":156},"      });\n",[146,6836,6837],{"class":148,"line":5333},[146,6838,189],{"emptyLinePlaceholder":188},[146,6840,6841,6843,6845,6847,6850,6853],{"class":148,"line":5339},[146,6842,2683],{"class":152},[146,6844,6226],{"class":156},[146,6846,250],{"class":201},[146,6848,6849],{"class":156},", data: ",[146,6851,6852],{"class":163},"'SUCCESS'",[146,6854,472],{"class":156},[146,6856,6857],{"class":148,"line":5345},[146,6858,6859],{"class":156},"  });\n",[146,6861,6862],{"class":148,"line":5351},[146,6863,5448],{"class":156},[146,6865,6866],{"class":148,"line":5357},[146,6867,6868],{"class":2896},"  // stop logic\n",[146,6870,6871],{"class":148,"line":5369},[146,6872,1507],{"class":156},[10,6874,6875,6876,6879,6880,6883],{},"The Vue application then simply sets up ",[143,6877,6878],{},"ipcRenderer.on"," listeners for events like ",[143,6881,6882],{},"volume-bot:stats",". When the utility process completes a trade, it outputs a metric line. The main process intercepts it, strips the prefix, parses the JSON, and pushes it directly into the Vue component's state.",[10,6885,6886,6887,6890,6891,6894],{},"It results in a smooth desktop UI where the user can watch their P&L, successful trades, and active logs update in real time, while the heavy cryptographic signing and network retry loops run isolated in the background. Stopping the bot is equally safe, the frontend sends a ",[143,6888,6889],{},"trade:volume-bot:stop"," command, the main process calls ",[143,6892,6893],{},"workerThread.kill()",", and the worker performs its graceful shutdown logic before exiting.",[10,6896,6897,6898,6914,6915,33],{},"You may also notice the ",[143,6899,6900,6902,6905,6907,6910,6912],{"className":137,"language":140,"style":141},[146,6901,6119],{"class":208},[146,6903,6904],{"class":156},": () ",[146,6906,1879],{"class":152},[146,6908,6909],{"class":156}," string ",[146,6911,1840],{"class":152},[146,6913,1199],{"class":201}," function. Remember the decrypt and encrypt functions? They require the master password the user has defined (in this case when first opening the app) that it needs to \"provide his password every time you make sensitive actions\", well with Electron I completely eliminated that. When the app first starts, after the first onboarding, it asks the user for its password, and saves it in Electron's native ",[21,6916,6919],{"href":6917,"rel":6918},"https://www.electronjs.org/docs/latest/api/safe-storage",[25],[143,6920,6921],{},"safeStorage",[10,6923,6924,6926],{},[143,6925,6921],{}," is an API built directly into Electron that allows applications to store data securely by leveraging OS-level encryption (like Keychain on macOS, DPAPI on Windows, and Secret Service API on Linux).",[10,6928,6929,6930,6932],{},"Instead of forcing the user to type their master password every single time they want to buy, sell, or view a wallet's private key, the app asks for it once per session. It then encrypts that master password using ",[143,6931,6921],{}," and holds it in memory within the main process as a base64 string (the vault variable).",[10,6934,6935,6936,501],{},"Here is how that looks in ",[143,6937,3833],{},[135,6939,6941],{"className":137,"code":6940,"filename":3833,"language":140,"meta":141,"style":141},"import { ipcMain, safeStorage } from 'electron';\n\nlet vault: string | null = null;\n\n//  inside app.whenReady() \n\nipcMain.handle('save-password', (_event, password: string) => {\n    // verify the OS supports native encryption\n    if (!safeStorage.isEncryptionAvailable()) {\n        return { ok: false, reason: 'OS_NOT_SUPPORTED' };\n    }\n    \n    try {\n        // Encrypt the password\n        const encrypted = safeStorage.encryptString(password);\n        \n        // Keep it in memory for the duration of the session\n        vault = encrypted.toString('base64'); \n\n        return { ok: true, data: 'Password saved' };\n    } catch {\n        return { ok: false, reason: 'ENCRYPTION_FAILED' };\n    }\n});\n\n// A quick helper to let the UI know if the user is \"logged in\"\nipcMain.handle('password-status', () => vault ? true : false);\n",[143,6942,6943,6956,6960,6980,6984,6989,6993,7023,7028,7045,7061,7065,7069,7075,7080,7095,7099,7104,7122,7126,7141,7149,7164,7168,7172,7176,7181],{"__ignoreMap":141},[146,6944,6945,6947,6950,6952,6954],{"class":148,"line":149},[146,6946,153],{"class":152},[146,6948,6949],{"class":156}," { ipcMain, safeStorage } ",[146,6951,160],{"class":152},[146,6953,6070],{"class":163},[146,6955,167],{"class":156},[146,6957,6958],{"class":148,"line":170},[146,6959,189],{"emptyLinePlaceholder":188},[146,6961,6962,6964,6966,6968,6970,6972,6974,6976,6978],{"class":148,"line":185},[146,6963,4076],{"class":152},[146,6965,6202],{"class":156},[146,6967,501],{"class":152},[146,6969,515],{"class":201},[146,6971,6131],{"class":152},[146,6973,1199],{"class":201},[146,6975,205],{"class":152},[146,6977,1199],{"class":201},[146,6979,167],{"class":156},[146,6981,6982],{"class":148,"line":192},[146,6983,189],{"emptyLinePlaceholder":188},[146,6985,6986],{"class":148,"line":221},[146,6987,6988],{"class":2896},"//  inside app.whenReady() \n",[146,6990,6991],{"class":148,"line":256},[146,6992,189],{"emptyLinePlaceholder":188},[146,6994,6995,6998,7000,7002,7005,7007,7009,7011,7013,7015,7017,7019,7021],{"class":148,"line":279},[146,6996,6997],{"class":156},"ipcMain.",[146,6999,6147],{"class":208},[146,7001,212],{"class":156},[146,7003,7004],{"class":163},"'save-password'",[146,7006,6456],{"class":156},[146,7008,6161],{"class":497},[146,7010,507],{"class":156},[146,7012,510],{"class":497},[146,7014,501],{"class":152},[146,7016,515],{"class":201},[146,7018,1876],{"class":156},[146,7020,1879],{"class":152},[146,7022,424],{"class":156},[146,7024,7025],{"class":148,"line":300},[146,7026,7027],{"class":2896},"    // verify the OS supports native encryption\n",[146,7029,7030,7032,7034,7036,7039,7042],{"class":148,"line":321},[146,7031,903],{"class":152},[146,7033,494],{"class":156},[146,7035,1712],{"class":152},[146,7037,7038],{"class":156},"safeStorage.",[146,7040,7041],{"class":208},"isEncryptionAvailable",[146,7043,7044],{"class":156},"()) {\n",[146,7046,7047,7049,7051,7053,7055,7058],{"class":148,"line":355},[146,7048,2120],{"class":152},[146,7050,6226],{"class":156},[146,7052,3773],{"class":201},[146,7054,6231],{"class":156},[146,7056,7057],{"class":163},"'OS_NOT_SUPPORTED'",[146,7059,7060],{"class":156}," };\n",[146,7062,7063],{"class":148,"line":562},[146,7064,938],{"class":156},[146,7066,7067],{"class":148,"line":584},[146,7068,615],{"class":156},[146,7070,7071,7073],{"class":148,"line":612},[146,7072,1763],{"class":152},[146,7074,424],{"class":156},[146,7076,7077],{"class":148,"line":618},[146,7078,7079],{"class":2896},"        // Encrypt the password\n",[146,7081,7082,7084,7086,7088,7090,7093],{"class":148,"line":641},[146,7083,1770],{"class":152},[146,7085,646],{"class":201},[146,7087,205],{"class":152},[146,7089,6248],{"class":156},[146,7091,7092],{"class":208},"encryptString",[146,7094,4603],{"class":156},[146,7096,7097],{"class":148,"line":672},[146,7098,6441],{"class":156},[146,7100,7101],{"class":148,"line":691},[146,7102,7103],{"class":2896},"        // Keep it in memory for the duration of the session\n",[146,7105,7106,7109,7111,7113,7115,7117,7119],{"class":148,"line":696},[146,7107,7108],{"class":156},"        vault ",[146,7110,955],{"class":152},[146,7112,972],{"class":156},[146,7114,3087],{"class":208},[146,7116,212],{"class":156},[146,7118,6261],{"class":163},[146,7120,7121],{"class":156},"); \n",[146,7123,7124],{"class":148,"line":709},[146,7125,189],{"emptyLinePlaceholder":188},[146,7127,7128,7130,7132,7134,7136,7139],{"class":148,"line":721},[146,7129,2120],{"class":152},[146,7131,6226],{"class":156},[146,7133,250],{"class":201},[146,7135,6849],{"class":156},[146,7137,7138],{"class":163},"'Password saved'",[146,7140,7060],{"class":156},[146,7142,7143,7145,7147],{"class":148,"line":758},[146,7144,2127],{"class":156},[146,7146,715],{"class":152},[146,7148,424],{"class":156},[146,7150,7151,7153,7155,7157,7159,7162],{"class":148,"line":764},[146,7152,2120],{"class":152},[146,7154,6226],{"class":156},[146,7156,3773],{"class":201},[146,7158,6231],{"class":156},[146,7160,7161],{"class":163},"'ENCRYPTION_FAILED'",[146,7163,7060],{"class":156},[146,7165,7166],{"class":148,"line":769},[146,7167,938],{"class":156},[146,7169,7170],{"class":148,"line":774},[146,7171,358],{"class":156},[146,7173,7174],{"class":148,"line":813},[146,7175,189],{"emptyLinePlaceholder":188},[146,7177,7178],{"class":148,"line":820},[146,7179,7180],{"class":2896},"// A quick helper to let the UI know if the user is \"logged in\"\n",[146,7182,7183,7185,7187,7189,7192,7194,7196,7199,7201,7203,7205,7207],{"class":148,"line":825},[146,7184,6997],{"class":156},[146,7186,6147],{"class":208},[146,7188,212],{"class":156},[146,7190,7191],{"class":163},"'password-status'",[146,7193,4508],{"class":156},[146,7195,1879],{"class":152},[146,7197,7198],{"class":156}," vault ",[146,7200,3667],{"class":152},[146,7202,4522],{"class":201},[146,7204,3674],{"class":152},[146,7206,5588],{"class":201},[146,7208,559],{"class":156},[10,7210,7211],{},"This completely solves the UX problem of the CLI. When the user initiates a sensitive action (like mass selling or starting the volume bot), the main process simply decrypts the vault string back into the master password and passes it quietly to the background workers. The user gets a seamless \"one-click\" trading experience, while the underlying design remains completely encrypted at rest.",[375,7213,7215],{"id":7214},"the-operations-worker","The Operations Worker",[10,7217,7218],{},"With the volume bot isolated in its own utility process and the password securely stored, I noticed one final performance bottleneck. Even standard operations, like decrypting 50 wallets just to view their public addresses in the UI table, or parsing database positions, were causing slight UI stutters because they were running on the main Electron process.",[10,7220,7221],{},"Increasing that number to 100, and boom, the app crashed.",[10,7223,7224],{},"To fix this, I completely removed all database and heavy cryptographic logic from the main process. I spawned a second background utility process dedicated entirely to handling SQLite database reads/writes and standard trade executions.",[10,7226,7227,7228,7231],{},"I created a wrapper called ",[143,7229,7230],{},"routeOps"," that generates a unique UUID for each request, sends the action and the decrypted master password to the background worker, and waits for the specific response to come back:",[135,7233,7236],{"className":137,"code":7234,"filename":7235,"language":140,"meta":141,"style":141},"import { utilityProcess } from \"electron\";\nimport crypto from 'node:crypto';\n\n// ... (worker spawning logic) ...\n\nexport const routeOps = \u003CT>(action: string, decryptedPassword?: string, args: unknown[] = []) => {\n  return new Promise\u003CResultsWithId\u003CT>>((resolve) => {\n      const requestId = crypto.randomUUID();\n      const dbProcess = getDbProcess();\n\n      const handleResponse = (payload: unknown) => {    \n        const data = payload as ResultsWithId\u003CT>; \n        if (data.id !== requestId) return; // ignore messages not meant for this request\n        \n        dbProcess.off('message', handleResponse);\n        resolve(data);\n      };\n      \n    dbProcess.on('message', handleResponse);\n    \n    // send the work to the background process\n    dbProcess.postMessage({ \n      id: requestId,\n      action, \n      decryptedPassword, \n      args, \n    });\n  });\n};\n","spawnOpWorker.ts",[143,7237,7238,7252,7266,7270,7275,7279,7333,7362,7378,7392,7396,7421,7445,7467,7471,7487,7495,7500,7504,7517,7521,7526,7536,7541,7546,7551,7556,7560,7564],{"__ignoreMap":141},[146,7239,7240,7242,7245,7247,7250],{"class":148,"line":149},[146,7241,153],{"class":152},[146,7243,7244],{"class":156}," { utilityProcess } ",[146,7246,160],{"class":152},[146,7248,7249],{"class":163}," \"electron\"",[146,7251,167],{"class":156},[146,7253,7254,7256,7259,7261,7264],{"class":148,"line":170},[146,7255,153],{"class":152},[146,7257,7258],{"class":156}," crypto ",[146,7260,160],{"class":152},[146,7262,7263],{"class":163}," 'node:crypto'",[146,7265,167],{"class":156},[146,7267,7268],{"class":148,"line":185},[146,7269,189],{"emptyLinePlaceholder":188},[146,7271,7272],{"class":148,"line":192},[146,7273,7274],{"class":2896},"// ... (worker spawning logic) ...\n",[146,7276,7277],{"class":148,"line":221},[146,7278,189],{"emptyLinePlaceholder":188},[146,7280,7281,7283,7285,7288,7290,7292,7295,7298,7301,7303,7305,7307,7310,7312,7314,7316,7318,7320,7322,7324,7326,7329,7331],{"class":148,"line":256},[146,7282,195],{"class":152},[146,7284,198],{"class":152},[146,7286,7287],{"class":208}," routeOps",[146,7289,205],{"class":152},[146,7291,912],{"class":156},[146,7293,7294],{"class":208},"T",[146,7296,7297],{"class":156},">(",[146,7299,7300],{"class":497},"action",[146,7302,501],{"class":152},[146,7304,515],{"class":201},[146,7306,507],{"class":156},[146,7308,7309],{"class":497},"decryptedPassword",[146,7311,2500],{"class":152},[146,7313,515],{"class":201},[146,7315,507],{"class":156},[146,7317,5141],{"class":497},[146,7319,501],{"class":152},[146,7321,1168],{"class":201},[146,7323,1837],{"class":156},[146,7325,955],{"class":152},[146,7327,7328],{"class":156}," []) ",[146,7330,1879],{"class":152},[146,7332,424],{"class":156},[146,7334,7335,7338,7340,7342,7344,7347,7349,7351,7354,7356,7358,7360],{"class":148,"line":279},[146,7336,7337],{"class":152},"  return",[146,7339,727],{"class":152},[146,7341,2665],{"class":201},[146,7343,4114],{"class":156},[146,7345,7346],{"class":208},"ResultsWithId",[146,7348,4114],{"class":156},[146,7350,7294],{"class":208},[146,7352,7353],{"class":156},">>((",[146,7355,1667],{"class":497},[146,7357,1876],{"class":156},[146,7359,1879],{"class":152},[146,7361,424],{"class":156},[146,7363,7364,7366,7369,7371,7373,7376],{"class":148,"line":300},[146,7365,2562],{"class":152},[146,7367,7368],{"class":201}," requestId",[146,7370,205],{"class":152},[146,7372,548],{"class":156},[146,7374,7375],{"class":208},"randomUUID",[146,7377,688],{"class":156},[146,7379,7380,7382,7385,7387,7390],{"class":148,"line":321},[146,7381,2562],{"class":152},[146,7383,7384],{"class":201}," dbProcess",[146,7386,205],{"class":152},[146,7388,7389],{"class":208}," getDbProcess",[146,7391,688],{"class":156},[146,7393,7394],{"class":148,"line":355},[146,7395,189],{"emptyLinePlaceholder":188},[146,7397,7398,7400,7403,7405,7407,7410,7412,7414,7416,7418],{"class":148,"line":562},[146,7399,2562],{"class":152},[146,7401,7402],{"class":208}," handleResponse",[146,7404,205],{"class":152},[146,7406,494],{"class":156},[146,7408,7409],{"class":497},"payload",[146,7411,501],{"class":152},[146,7413,1168],{"class":201},[146,7415,1876],{"class":156},[146,7417,1879],{"class":152},[146,7419,7420],{"class":156}," {    \n",[146,7422,7423,7425,7428,7430,7433,7435,7438,7440,7442],{"class":148,"line":584},[146,7424,1770],{"class":152},[146,7426,7427],{"class":201}," data",[146,7429,205],{"class":152},[146,7431,7432],{"class":156}," payload ",[146,7434,1217],{"class":152},[146,7436,7437],{"class":208}," ResultsWithId",[146,7439,4114],{"class":156},[146,7441,7294],{"class":208},[146,7443,7444],{"class":156},">; \n",[146,7446,7447,7450,7453,7456,7459,7461,7464],{"class":148,"line":612},[146,7448,7449],{"class":152},"        if",[146,7451,7452],{"class":156}," (data.id ",[146,7454,7455],{"class":152},"!==",[146,7457,7458],{"class":156}," requestId) ",[146,7460,6223],{"class":152},[146,7462,7463],{"class":156},"; ",[146,7465,7466],{"class":2896},"// ignore messages not meant for this request\n",[146,7468,7469],{"class":148,"line":618},[146,7470,6441],{"class":156},[146,7472,7473,7476,7479,7481,7484],{"class":148,"line":641},[146,7474,7475],{"class":156},"        dbProcess.",[146,7477,7478],{"class":208},"off",[146,7480,212],{"class":156},[146,7482,7483],{"class":163},"'message'",[146,7485,7486],{"class":156},", handleResponse);\n",[146,7488,7489,7492],{"class":148,"line":672},[146,7490,7491],{"class":208},"        resolve",[146,7493,7494],{"class":156},"(data);\n",[146,7496,7497],{"class":148,"line":691},[146,7498,7499],{"class":156},"      };\n",[146,7501,7502],{"class":148,"line":696},[146,7503,6406],{"class":156},[146,7505,7506,7509,7511,7513,7515],{"class":148,"line":709},[146,7507,7508],{"class":156},"    dbProcess.",[146,7510,4500],{"class":208},[146,7512,212],{"class":156},[146,7514,7483],{"class":163},[146,7516,7486],{"class":156},[146,7518,7519],{"class":148,"line":721},[146,7520,615],{"class":156},[146,7522,7523],{"class":148,"line":758},[146,7524,7525],{"class":2896},"    // send the work to the background process\n",[146,7527,7528,7530,7533],{"class":148,"line":764},[146,7529,7508],{"class":156},[146,7531,7532],{"class":208},"postMessage",[146,7534,7535],{"class":156},"({ \n",[146,7537,7538],{"class":148,"line":769},[146,7539,7540],{"class":156},"      id: requestId,\n",[146,7542,7543],{"class":148,"line":774},[146,7544,7545],{"class":156},"      action, \n",[146,7547,7548],{"class":148,"line":813},[146,7549,7550],{"class":156},"      decryptedPassword, \n",[146,7552,7553],{"class":148,"line":820},[146,7554,7555],{"class":156},"      args, \n",[146,7557,7558],{"class":148,"line":825},[146,7559,1477],{"class":156},[146,7561,7562],{"class":148,"line":840},[146,7563,6859],{"class":156},[146,7565,7566],{"class":148,"line":856},[146,7567,472],{"class":156},[10,7569,7570,7571,7574,7575,7577],{},"Then, inside the ",[143,7572,7573],{},"OperationWorker.ts"," file, an if/else block intercepts these actions, runs the heavy ",[143,7576,404],{}," hashing to unlock the SQLite wallets, interacts with the blockchain, and sends the payload back:",[135,7579,7581],{"className":137,"code":7580,"filename":7573,"language":140,"meta":141,"style":141},"process.parentPort.on('message', async (event) => {\n      const { id, action, args, decryptedPassword } = event.data as WorkerRequest;\n      \n      \n      try {\n        let result;\n        if (action === 'get:wallets:all') {\n          result = await getAllWalletsView(decryptedPassword);\n        }\n\n        else if (action === 'get:wallets:management') {\n          result = await getWalletsManagementView(decryptedPassword);\n        } \n\n        else if (action === 'get:wallets:one') {\n          if (!args[0] || typeof args[0] !== 'string') {\n            throw new Error('Invalid arguments: Expected a string public key');\n          } else {\n            result = await getByPubkey(decryptedPassword, args[0]);\n          }\n        }   \n        // ... other actions\n\n     // catch all errors and normalize results\n    if (result !== null && typeof result === 'object' && 'ok' in result) {\n      if (!result.ok) {\n        throw new Error(result.reason ?? 'Underlying operation failed');\n      }\n      if ('data' in result) result = result.data;\n      else {\n        throw new Error(result.reason ?? 'Unexpected data type');\n      }\n    }\n      // send\n      process.parentPort.postMessage({ \n        id, \n        ok: true, \n        date: new Date().toISOString(), \n        data: result \n      });\n\n      } catch (err) {\n        // send caught error\n        const message = err instanceof Error ? err.message : String(err);\n        process.parentPort.postMessage({ \n            id, \n            ok: false, \n            date: new Date().toISOString(), \n            reason: message \n        });\n      }\n})\n",[143,7582,7583,7609,7646,7650,7654,7661,7668,7682,7697,7701,7705,7721,7734,7739,7743,7758,7790,7806,7814,7834,7838,7843,7848,7852,7857,7891,7903,7921,7925,7944,7951,7968,7972,7976,7981,7990,7995,8003,8022,8027,8031,8035,8044,8049,8076,8085,8090,8099,8114,8119,8123,8127],{"__ignoreMap":141},[146,7584,7585,7588,7590,7592,7594,7596,7598,7600,7603,7605,7607],{"class":148,"line":149},[146,7586,7587],{"class":156},"process.parentPort.",[146,7589,4500],{"class":208},[146,7591,212],{"class":156},[146,7593,7483],{"class":163},[146,7595,507],{"class":156},[146,7597,2832],{"class":152},[146,7599,494],{"class":156},[146,7601,7602],{"class":497},"event",[146,7604,1876],{"class":156},[146,7606,1879],{"class":152},[146,7608,424],{"class":156},[146,7610,7611,7613,7616,7619,7621,7623,7625,7627,7629,7631,7634,7636,7639,7641,7644],{"class":148,"line":170},[146,7612,2562],{"class":152},[146,7614,7615],{"class":156}," { ",[146,7617,7618],{"class":201},"id",[146,7620,507],{"class":156},[146,7622,7300],{"class":201},[146,7624,507],{"class":156},[146,7626,5141],{"class":201},[146,7628,507],{"class":156},[146,7630,7309],{"class":201},[146,7632,7633],{"class":156}," } ",[146,7635,955],{"class":152},[146,7637,7638],{"class":156}," event.data ",[146,7640,1217],{"class":152},[146,7642,7643],{"class":208}," WorkerRequest",[146,7645,167],{"class":156},[146,7647,7648],{"class":148,"line":185},[146,7649,6406],{"class":156},[146,7651,7652],{"class":148,"line":192},[146,7653,6406],{"class":156},[146,7655,7656,7659],{"class":148,"line":221},[146,7657,7658],{"class":152},"      try",[146,7660,424],{"class":156},[146,7662,7663,7665],{"class":148,"line":256},[146,7664,1788],{"class":152},[146,7666,7667],{"class":156}," result;\n",[146,7669,7670,7672,7675,7677,7680],{"class":148,"line":279},[146,7671,7449],{"class":152},[146,7673,7674],{"class":156}," (action ",[146,7676,1239],{"class":152},[146,7678,7679],{"class":163}," 'get:wallets:all'",[146,7681,1171],{"class":156},[146,7683,7684,7687,7689,7691,7694],{"class":148,"line":300},[146,7685,7686],{"class":156},"          result ",[146,7688,955],{"class":152},[146,7690,1571],{"class":152},[146,7692,7693],{"class":208}," getAllWalletsView",[146,7695,7696],{"class":156},"(decryptedPassword);\n",[146,7698,7699],{"class":148,"line":321},[146,7700,2012],{"class":156},[146,7702,7703],{"class":148,"line":355},[146,7704,189],{"emptyLinePlaceholder":188},[146,7706,7707,7710,7712,7714,7716,7719],{"class":148,"line":562},[146,7708,7709],{"class":152},"        else",[146,7711,1922],{"class":152},[146,7713,7674],{"class":156},[146,7715,1239],{"class":152},[146,7717,7718],{"class":163}," 'get:wallets:management'",[146,7720,1171],{"class":156},[146,7722,7723,7725,7727,7729,7732],{"class":148,"line":584},[146,7724,7686],{"class":156},[146,7726,955],{"class":152},[146,7728,1571],{"class":152},[146,7730,7731],{"class":208}," getWalletsManagementView",[146,7733,7696],{"class":156},[146,7735,7736],{"class":148,"line":612},[146,7737,7738],{"class":156},"        } \n",[146,7740,7741],{"class":148,"line":618},[146,7742,189],{"emptyLinePlaceholder":188},[146,7744,7745,7747,7749,7751,7753,7756],{"class":148,"line":641},[146,7746,7709],{"class":152},[146,7748,1922],{"class":152},[146,7750,7674],{"class":156},[146,7752,1239],{"class":152},[146,7754,7755],{"class":163}," 'get:wallets:one'",[146,7757,1171],{"class":156},[146,7759,7760,7762,7764,7766,7769,7771,7773,7775,7777,7780,7782,7784,7786,7788],{"class":148,"line":672},[146,7761,6485],{"class":152},[146,7763,494],{"class":156},[146,7765,1712],{"class":152},[146,7767,7768],{"class":156},"args[",[146,7770,5197],{"class":201},[146,7772,1566],{"class":156},[146,7774,1612],{"class":152},[146,7776,1882],{"class":152},[146,7778,7779],{"class":156}," args[",[146,7781,5197],{"class":201},[146,7783,1566],{"class":156},[146,7785,7455],{"class":152},[146,7787,1935],{"class":163},[146,7789,1171],{"class":156},[146,7791,7792,7795,7797,7799,7801,7804],{"class":148,"line":691},[146,7793,7794],{"class":152},"            throw",[146,7796,727],{"class":152},[146,7798,730],{"class":208},[146,7800,212],{"class":156},[146,7802,7803],{"class":163},"'Invalid arguments: Expected a string public key'",[146,7805,559],{"class":156},[146,7807,7808,7810,7812],{"class":148,"line":696},[146,7809,6733],{"class":156},[146,7811,1919],{"class":152},[146,7813,424],{"class":156},[146,7815,7816,7819,7821,7823,7826,7829,7831],{"class":148,"line":709},[146,7817,7818],{"class":156},"            result ",[146,7820,955],{"class":152},[146,7822,1571],{"class":152},[146,7824,7825],{"class":208}," getByPubkey",[146,7827,7828],{"class":156},"(decryptedPassword, args[",[146,7830,5197],{"class":201},[146,7832,7833],{"class":156},"]);\n",[146,7835,7836],{"class":148,"line":721},[146,7837,6762],{"class":156},[146,7839,7840],{"class":148,"line":758},[146,7841,7842],{"class":156},"        }   \n",[146,7844,7845],{"class":148,"line":764},[146,7846,7847],{"class":2896},"        // ... other actions\n",[146,7849,7850],{"class":148,"line":769},[146,7851,189],{"emptyLinePlaceholder":188},[146,7853,7854],{"class":148,"line":774},[146,7855,7856],{"class":2896},"     // catch all errors and normalize results\n",[146,7858,7859,7861,7864,7866,7868,7870,7872,7875,7877,7880,7882,7885,7888],{"class":148,"line":813},[146,7860,903],{"class":152},[146,7862,7863],{"class":156}," (result ",[146,7865,7455],{"class":152},[146,7867,1199],{"class":201},[146,7869,2543],{"class":152},[146,7871,1882],{"class":152},[146,7873,7874],{"class":156}," result ",[146,7876,1239],{"class":152},[146,7878,7879],{"class":163}," 'object'",[146,7881,2543],{"class":152},[146,7883,7884],{"class":163}," 'ok'",[146,7886,7887],{"class":152}," in",[146,7889,7890],{"class":156}," result) {\n",[146,7892,7893,7896,7898,7900],{"class":148,"line":820},[146,7894,7895],{"class":152},"      if",[146,7897,494],{"class":156},[146,7899,1712],{"class":152},[146,7901,7902],{"class":156},"result.ok) {\n",[146,7904,7905,7907,7909,7911,7914,7916,7919],{"class":148,"line":825},[146,7906,1634],{"class":152},[146,7908,727],{"class":152},[146,7910,730],{"class":208},[146,7912,7913],{"class":156},"(result.reason ",[146,7915,3967],{"class":152},[146,7917,7918],{"class":163}," 'Underlying operation failed'",[146,7920,559],{"class":156},[146,7922,7923],{"class":148,"line":840},[146,7924,6771],{"class":156},[146,7926,7927,7929,7931,7934,7936,7939,7941],{"class":148,"line":856},[146,7928,7895],{"class":152},[146,7930,494],{"class":156},[146,7932,7933],{"class":163},"'data'",[146,7935,7887],{"class":152},[146,7937,7938],{"class":156}," result) result ",[146,7940,955],{"class":152},[146,7942,7943],{"class":156}," result.data;\n",[146,7945,7946,7949],{"class":148,"line":871},[146,7947,7948],{"class":152},"      else",[146,7950,424],{"class":156},[146,7952,7953,7955,7957,7959,7961,7963,7966],{"class":148,"line":895},[146,7954,1634],{"class":152},[146,7956,727],{"class":152},[146,7958,730],{"class":208},[146,7960,7913],{"class":156},[146,7962,3967],{"class":152},[146,7964,7965],{"class":163}," 'Unexpected data type'",[146,7967,559],{"class":156},[146,7969,7970],{"class":148,"line":900},[146,7971,6771],{"class":156},[146,7973,7974],{"class":148,"line":918},[146,7975,938],{"class":156},[146,7977,7978],{"class":148,"line":935},[146,7979,7980],{"class":2896},"      // send\n",[146,7982,7983,7986,7988],{"class":148,"line":941},[146,7984,7985],{"class":156},"      process.parentPort.",[146,7987,7532],{"class":208},[146,7989,7535],{"class":156},[146,7991,7992],{"class":148,"line":946},[146,7993,7994],{"class":156},"        id, \n",[146,7996,7997,7999,8001],{"class":148,"line":963},[146,7998,3770],{"class":156},[146,8000,250],{"class":201},[146,8002,3776],{"class":156},[146,8004,8005,8008,8011,8014,8016,8019],{"class":148,"line":987},[146,8006,8007],{"class":156},"        date: ",[146,8009,8010],{"class":152},"new",[146,8012,8013],{"class":208}," Date",[146,8015,338],{"class":156},[146,8017,8018],{"class":208},"toISOString",[146,8020,8021],{"class":156},"(), \n",[146,8023,8024],{"class":148,"line":1007},[146,8025,8026],{"class":156},"        data: result \n",[146,8028,8029],{"class":148,"line":1027},[146,8030,6834],{"class":156},[146,8032,8033],{"class":148,"line":1044},[146,8034,189],{"emptyLinePlaceholder":188},[146,8036,8037,8040,8042],{"class":148,"line":1049},[146,8038,8039],{"class":156},"      } ",[146,8041,715],{"class":152},[146,8043,718],{"class":156},[146,8045,8046],{"class":148,"line":1072},[146,8047,8048],{"class":2896},"        // send caught error\n",[146,8050,8051,8053,8056,8058,8061,8063,8065,8067,8070,8072,8074],{"class":148,"line":1093},[146,8052,1770],{"class":152},[146,8054,8055],{"class":201}," message",[146,8057,205],{"class":152},[146,8059,8060],{"class":156}," err ",[146,8062,1187],{"class":152},[146,8064,730],{"class":208},[146,8066,1192],{"class":152},[146,8068,8069],{"class":156}," err.message ",[146,8071,501],{"class":152},[146,8073,6376],{"class":208},[146,8075,2153],{"class":156},[146,8077,8078,8081,8083],{"class":148,"line":1105},[146,8079,8080],{"class":156},"        process.parentPort.",[146,8082,7532],{"class":208},[146,8084,7535],{"class":156},[146,8086,8087],{"class":148,"line":1110},[146,8088,8089],{"class":156},"            id, \n",[146,8091,8092,8095,8097],{"class":148,"line":1127},[146,8093,8094],{"class":156},"            ok: ",[146,8096,3773],{"class":201},[146,8098,3776],{"class":156},[146,8100,8101,8104,8106,8108,8110,8112],{"class":148,"line":1138},[146,8102,8103],{"class":156},"            date: ",[146,8105,8010],{"class":152},[146,8107,8013],{"class":208},[146,8109,338],{"class":156},[146,8111,8018],{"class":208},[146,8113,8021],{"class":156},[146,8115,8116],{"class":148,"line":1148},[146,8117,8118],{"class":156},"            reason: message \n",[146,8120,8121],{"class":148,"line":1154},[146,8122,2092],{"class":156},[146,8124,8125],{"class":148,"line":1174},[146,8126,6771],{"class":156},[146,8128,8129],{"class":148,"line":1204},[146,8130,5890],{"class":156},[375,8132,8134],{"id":8133},"the-build-process","The Build Process",[10,8136,8137],{},"What started as a \"simple script to trade meme coins\" quickly grew into a deep dive into cryptography, state management, exponential backoffs, IPC bridges, and multi-process architecture.",[10,8139,8140],{},"Building a local-first application on Solana that handles high-concurrency mass operations securely is what I discovered a complex challenge. You cannot rely strictly on the blockchain RPCs for state when dealing with mass operations, and you cannot run heavy cryptography on a UI thread.",[10,8142,8143,8144,8146,8147,8149],{},"Using SQLite for local state tracking, ",[143,8145,400],{}," for wallet encryption, Electron's ",[143,8148,6921],{}," for session memory, and detached workers for execution, the resulting application ended up, resilient to rate limits, and safe enough for non-technical users.",[10,8151,8152],{},"With that said, when it was time to finally package all of this with Electron Forge, it very quickly became a massive headache.",[375,8154,8156],{"id":8155},"multi-process-vite-the-asar-archive","Multi-Process Vite & The ASAR Archive",[10,8158,8159,8160,8167],{},"Because I was using multiple detached utility processes (one for the Volume Bot, one for the Database Operations), each worker needed its own isolated entry point in the final compiled ",[21,8161,8164],{"href":8162,"rel":8163},"https://www.electronjs.org/docs/latest/tutorial/asar-archives",[25],[143,8165,8166],{},"app.asar"," archive.",[10,8169,8170],{},"Vite is great for frontends, but handling multiple Node.js backend scripts required creating completely separate configurations (vite.opworker.config.ts, vite.worker.config.ts). I had to explicitly configure Vite to bundle them for a Node environment by using SSR flags and preventing externalization:",[135,8172,8175],{"className":137,"code":8173,"filename":8174,"language":140,"meta":141,"style":141},"import { ignoreBunPlugin } from './vite.main.config';\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n        resolve: {\n        alias: {\n          '@solana-bots': path.resolve(__dirname, '../src'),\n          '@': path.resolve(__dirname, 'src'),\n        },\n      },\n  ssr: {\n    noExternal: true // Bundle everything\n  },\n    plugins: [ignoreBunPlugin],\n    optimizeDeps: {\n    exclude: ['bun:sqlite', 'drizzle-orm/bun-sqlite', 'drizzle-orm/bun-sqlite/migrator'] // vite still trys to optimize these even if they are external\n  },\n  build: {\n    outDir: 'dist-worker/ops',\n    ssr: true,\n    target: 'node20',\n    lib: {\n      entry: 'src/electron/utils/OperationWorker.ts',\n      formats: ['es'],\n    },\n    rollupOptions: {\n      external: [\n        'electron', \n        'better-sqlite3', // ignore native dependencies\n        'bun:sqlite',\n        'drizzle-orm/bun-sqlite', // ignore the cli adapters\n        'drizzle-orm/bun-sqlite/migrator',\n      ],\n      output: {\n        entryFileNames: '[name].mjs',\n        inlineDynamicImports: true,\n      },\n    },\n  },\n});\n","vite.opworker.config.ts",[143,8176,8177,8191,8222,8226,8238,8243,8248,8266,8282,8286,8291,8296,8306,8310,8315,8320,8343,8347,8352,8362,8371,8381,8386,8396,8407,8411,8416,8421,8428,8438,8445,8455,8462,8467,8472,8482,8491,8495,8499,8503],{"__ignoreMap":141},[146,8178,8179,8181,8184,8186,8189],{"class":148,"line":149},[146,8180,153],{"class":152},[146,8182,8183],{"class":156}," { ignoreBunPlugin } ",[146,8185,160],{"class":152},[146,8187,8188],{"class":163}," './vite.main.config'",[146,8190,167],{"class":156},[146,8192,8193,8195,8198,8200,8202,8205,8207,8210,8212,8214,8216,8219],{"class":148,"line":170},[146,8194,416],{"class":152},[146,8196,8197],{"class":201}," __dirname",[146,8199,205],{"class":152},[146,8201,1664],{"class":156},[146,8203,8204],{"class":208},"dirname",[146,8206,212],{"class":156},[146,8208,8209],{"class":208},"fileURLToPath",[146,8211,212],{"class":156},[146,8213,153],{"class":152},[146,8215,33],{"class":156},[146,8217,8218],{"class":201},"meta",[146,8220,8221],{"class":156},".url));\n",[146,8223,8224],{"class":148,"line":185},[146,8225,189],{"emptyLinePlaceholder":188},[146,8227,8228,8230,8233,8236],{"class":148,"line":192},[146,8229,195],{"class":152},[146,8231,8232],{"class":152}," default",[146,8234,8235],{"class":208}," defineConfig",[146,8237,1449],{"class":156},[146,8239,8240],{"class":148,"line":221},[146,8241,8242],{"class":156},"        resolve: {\n",[146,8244,8245],{"class":148,"line":256},[146,8246,8247],{"class":156},"        alias: {\n",[146,8249,8250,8253,8256,8258,8261,8264],{"class":148,"line":279},[146,8251,8252],{"class":163},"          '@solana-bots'",[146,8254,8255],{"class":156},": path.",[146,8257,1667],{"class":208},[146,8259,8260],{"class":156},"(__dirname, ",[146,8262,8263],{"class":163},"'../src'",[146,8265,352],{"class":156},[146,8267,8268,8271,8273,8275,8277,8280],{"class":148,"line":300},[146,8269,8270],{"class":163},"          '@'",[146,8272,8255],{"class":156},[146,8274,1667],{"class":208},[146,8276,8260],{"class":156},[146,8278,8279],{"class":163},"'src'",[146,8281,352],{"class":156},[146,8283,8284],{"class":148,"line":321},[146,8285,6389],{"class":156},[146,8287,8288],{"class":148,"line":355},[146,8289,8290],{"class":156},"      },\n",[146,8292,8293],{"class":148,"line":562},[146,8294,8295],{"class":156},"  ssr: {\n",[146,8297,8298,8301,8303],{"class":148,"line":584},[146,8299,8300],{"class":156},"    noExternal: ",[146,8302,250],{"class":201},[146,8304,8305],{"class":2896}," // Bundle everything\n",[146,8307,8308],{"class":148,"line":612},[146,8309,4835],{"class":156},[146,8311,8312],{"class":148,"line":618},[146,8313,8314],{"class":156},"    plugins: [ignoreBunPlugin],\n",[146,8316,8317],{"class":148,"line":641},[146,8318,8319],{"class":156},"    optimizeDeps: {\n",[146,8321,8322,8325,8328,8330,8333,8335,8338,8340],{"class":148,"line":672},[146,8323,8324],{"class":156},"    exclude: [",[146,8326,8327],{"class":163},"'bun:sqlite'",[146,8329,507],{"class":156},[146,8331,8332],{"class":163},"'drizzle-orm/bun-sqlite'",[146,8334,507],{"class":156},[146,8336,8337],{"class":163},"'drizzle-orm/bun-sqlite/migrator'",[146,8339,1566],{"class":156},[146,8341,8342],{"class":2896},"// vite still trys to optimize these even if they are external\n",[146,8344,8345],{"class":148,"line":691},[146,8346,4835],{"class":156},[146,8348,8349],{"class":148,"line":696},[146,8350,8351],{"class":156},"  build: {\n",[146,8353,8354,8357,8360],{"class":148,"line":709},[146,8355,8356],{"class":156},"    outDir: ",[146,8358,8359],{"class":163},"'dist-worker/ops'",[146,8361,435],{"class":156},[146,8363,8364,8367,8369],{"class":148,"line":721},[146,8365,8366],{"class":156},"    ssr: ",[146,8368,250],{"class":201},[146,8370,435],{"class":156},[146,8372,8373,8376,8379],{"class":148,"line":758},[146,8374,8375],{"class":156},"    target: ",[146,8377,8378],{"class":163},"'node20'",[146,8380,435],{"class":156},[146,8382,8383],{"class":148,"line":764},[146,8384,8385],{"class":156},"    lib: {\n",[146,8387,8388,8391,8394],{"class":148,"line":769},[146,8389,8390],{"class":156},"      entry: ",[146,8392,8393],{"class":163},"'src/electron/utils/OperationWorker.ts'",[146,8395,435],{"class":156},[146,8397,8398,8401,8404],{"class":148,"line":774},[146,8399,8400],{"class":156},"      formats: [",[146,8402,8403],{"class":163},"'es'",[146,8405,8406],{"class":156},"],\n",[146,8408,8409],{"class":148,"line":813},[146,8410,3977],{"class":156},[146,8412,8413],{"class":148,"line":820},[146,8414,8415],{"class":156},"    rollupOptions: {\n",[146,8417,8418],{"class":148,"line":825},[146,8419,8420],{"class":156},"      external: [\n",[146,8422,8423,8426],{"class":148,"line":840},[146,8424,8425],{"class":163},"        'electron'",[146,8427,3776],{"class":156},[146,8429,8430,8433,8435],{"class":148,"line":856},[146,8431,8432],{"class":163},"        'better-sqlite3'",[146,8434,507],{"class":156},[146,8436,8437],{"class":2896},"// ignore native dependencies\n",[146,8439,8440,8443],{"class":148,"line":871},[146,8441,8442],{"class":163},"        'bun:sqlite'",[146,8444,435],{"class":156},[146,8446,8447,8450,8452],{"class":148,"line":895},[146,8448,8449],{"class":163},"        'drizzle-orm/bun-sqlite'",[146,8451,507],{"class":156},[146,8453,8454],{"class":2896},"// ignore the cli adapters\n",[146,8456,8457,8460],{"class":148,"line":900},[146,8458,8459],{"class":163},"        'drizzle-orm/bun-sqlite/migrator'",[146,8461,435],{"class":156},[146,8463,8464],{"class":148,"line":918},[146,8465,8466],{"class":156},"      ],\n",[146,8468,8469],{"class":148,"line":935},[146,8470,8471],{"class":156},"      output: {\n",[146,8473,8474,8477,8480],{"class":148,"line":941},[146,8475,8476],{"class":156},"        entryFileNames: ",[146,8478,8479],{"class":163},"'[name].mjs'",[146,8481,435],{"class":156},[146,8483,8484,8487,8489],{"class":148,"line":946},[146,8485,8486],{"class":156},"        inlineDynamicImports: ",[146,8488,250],{"class":201},[146,8490,435],{"class":156},[146,8492,8493],{"class":148,"line":963},[146,8494,8290],{"class":156},[146,8496,8497],{"class":148,"line":987},[146,8498,3977],{"class":156},[146,8500,8501],{"class":148,"line":1007},[146,8502,4835],{"class":156},[146,8504,8505],{"class":148,"line":1027},[146,8506,358],{"class":156},[10,8508,8509],{},"Both workers' configs look identical; what's different are the paths and entry points.",[10,8511,2403,8512,8515,8516,8519],{},[143,8513,8514],{},"ignoreBunPlugin"," plugin is a vite plugin defined in ",[143,8517,8518],{},"vite.main.config.ts"," to FORCE Vite to ignore bun:",[135,8521,8523],{"className":137,"code":8522,"filename":8518,"language":140,"meta":141,"style":141},"export const ignoreBunPlugin = {\n  name: 'ignore-bun-modules',\n  resolveId(id: string) {\n    if (id === 'bun:sqlite' || id.includes('bun-sqlite')) {\n      return { id, external: true };\n    }\n  }\n};\n",[143,8524,8525,8538,8548,8563,8589,8600,8604,8608],{"__ignoreMap":141},[146,8526,8527,8529,8531,8534,8536],{"class":148,"line":149},[146,8528,195],{"class":152},[146,8530,198],{"class":152},[146,8532,8533],{"class":201}," ignoreBunPlugin",[146,8535,205],{"class":152},[146,8537,424],{"class":156},[146,8539,8540,8543,8546],{"class":148,"line":170},[146,8541,8542],{"class":156},"  name: ",[146,8544,8545],{"class":163},"'ignore-bun-modules'",[146,8547,435],{"class":156},[146,8549,8550,8553,8555,8557,8559,8561],{"class":148,"line":185},[146,8551,8552],{"class":208},"  resolveId",[146,8554,212],{"class":156},[146,8556,7618],{"class":497},[146,8558,501],{"class":152},[146,8560,515],{"class":201},[146,8562,1171],{"class":156},[146,8564,8565,8567,8570,8572,8575,8577,8580,8582,8584,8587],{"class":148,"line":192},[146,8566,903],{"class":152},[146,8568,8569],{"class":156}," (id ",[146,8571,1239],{"class":152},[146,8573,8574],{"class":163}," 'bun:sqlite'",[146,8576,1245],{"class":152},[146,8578,8579],{"class":156}," id.",[146,8581,1256],{"class":208},[146,8583,212],{"class":156},[146,8585,8586],{"class":163},"'bun-sqlite'",[146,8588,1264],{"class":156},[146,8590,8591,8593,8596,8598],{"class":148,"line":221},[146,8592,2683],{"class":152},[146,8594,8595],{"class":156}," { id, external: ",[146,8597,250],{"class":201},[146,8599,7060],{"class":156},[146,8601,8602],{"class":148,"line":256},[146,8603,938],{"class":156},[146,8605,8606],{"class":148,"line":279},[146,8607,761],{"class":156},[146,8609,8610],{"class":148,"line":300},[146,8611,472],{"class":156},[10,8613,8614,8615,8618,8619,8622,8623,8625],{},"Furthermore, Electron's default ",[143,8616,8617],{},"electron-squirrel-startup"," module was crashing Vite's ESM build completely. I ended up having to write a custom ESM implementation of the startup script (",[143,8620,8621],{},"electronStartEsm.ts",") using Node's ",[143,8624,3828],{}," just to handle Windows shortcut creation properly without breaking the Vite pipeline.",[375,8627,8629],{"id":8628},"the-native-dependency","The Native Dependency",[10,8631,8632],{},"The biggest headache by far was SQLite.",[10,8634,8635],{},"For the CLI version, I used bun:sqlite because it is fast, built-in, and because compiling the CLI with better-sqlite3 using Bun would outright crash the binary. But Electron requires a Node-compatible native module, so for the desktop app, I had to use better-sqlite3. (While I could have used the Node adapter for everything, it would have required rewiring some of the queries and relying on a beta version of Drizzle ORM).",[10,8637,8638],{},"This requirement forced a dual-adapter pattern in the database connection file that checks the environment at runtime:",[135,8640,8643],{"className":137,"code":8641,"filename":8642,"language":140,"meta":141,"style":141},"const isBun = (typeof process.versions !== 'undefined' && process.versions.bun !== undefined) || process.env['CLI_BUILD'];\nlet db: AppDatabase;\n\nif (isBun) {\n  const { Database } = await import(/* @vite-ignore */ \"bun:sqlite\");\n  // ... bun setup\n} else {\n  const Database = (await import('better-sqlite3')).default;\n  // better-sqlite3 setup\n}\n","connection.ts",[143,8644,8645,8688,8702,8706,8713,8741,8746,8755,8778,8783],{"__ignoreMap":141},[146,8646,8647,8649,8652,8654,8656,8658,8661,8663,8666,8668,8671,8673,8675,8677,8679,8682,8685],{"class":148,"line":149},[146,8648,416],{"class":152},[146,8650,8651],{"class":201}," isBun",[146,8653,205],{"class":152},[146,8655,494],{"class":156},[146,8657,1927],{"class":152},[146,8659,8660],{"class":156}," process.versions ",[146,8662,7455],{"class":152},[146,8664,8665],{"class":163}," 'undefined'",[146,8667,2543],{"class":152},[146,8669,8670],{"class":156}," process.versions.bun ",[146,8672,7455],{"class":152},[146,8674,5615],{"class":201},[146,8676,1876],{"class":156},[146,8678,1612],{"class":152},[146,8680,8681],{"class":156}," process.env[",[146,8683,8684],{"class":163},"'CLI_BUILD'",[146,8686,8687],{"class":156},"];\n",[146,8689,8690,8692,8695,8697,8700],{"class":148,"line":170},[146,8691,4076],{"class":152},[146,8693,8694],{"class":156}," db",[146,8696,501],{"class":152},[146,8698,8699],{"class":208}," AppDatabase",[146,8701,167],{"class":156},[146,8703,8704],{"class":148,"line":185},[146,8705,189],{"emptyLinePlaceholder":188},[146,8707,8708,8710],{"class":148,"line":192},[146,8709,3439],{"class":152},[146,8711,8712],{"class":156}," (isBun) {\n",[146,8714,8715,8717,8719,8722,8724,8726,8728,8731,8733,8736,8739],{"class":148,"line":221},[146,8716,3642],{"class":152},[146,8718,7615],{"class":156},[146,8720,8721],{"class":201},"Database",[146,8723,7633],{"class":156},[146,8725,955],{"class":152},[146,8727,1571],{"class":152},[146,8729,8730],{"class":208}," import",[146,8732,212],{"class":156},[146,8734,8735],{"class":2896},"/* @vite-ignore */",[146,8737,8738],{"class":163}," \"bun:sqlite\"",[146,8740,559],{"class":156},[146,8742,8743],{"class":148,"line":256},[146,8744,8745],{"class":2896},"  // ... bun setup\n",[146,8747,8748,8751,8753],{"class":148,"line":279},[146,8749,8750],{"class":156},"} ",[146,8752,1919],{"class":152},[146,8754,424],{"class":156},[146,8756,8757,8759,8762,8764,8766,8768,8770,8772,8775],{"class":148,"line":300},[146,8758,3642],{"class":152},[146,8760,8761],{"class":201}," Database",[146,8763,205],{"class":152},[146,8765,494],{"class":156},[146,8767,2823],{"class":152},[146,8769,8730],{"class":152},[146,8771,212],{"class":156},[146,8773,8774],{"class":163},"'better-sqlite3'",[146,8776,8777],{"class":156},")).default;\n",[146,8779,8780],{"class":148,"line":321},[146,8781,8782],{"class":2896},"  // better-sqlite3 setup\n",[146,8784,8785],{"class":148,"line":355},[146,8786,1507],{"class":156},[10,8788,8789,8790,8793,8794,33],{},"This split immediately complicated database migrations. Because the CLI is compiled into a standalone binary and the UI is packaged into a read-only ASAR archive, the raw ",[143,8791,8792],{},".sql"," migration files generated by Drizzle had to be manually copied around during the build process and referenced differently depending on the environment. I had to write custom logic to locate the migrations folder dynamically—whether it was running in dev mode, from the compiled Bun executable directory, or from Electron's ",[143,8795,8796],{},"process.resourcesPath",[10,8798,8799,8800,8803,8804,8807],{},"This caused chaos during packaging. The Electron Forge Vite plugin couldn't properly detect and package the native C++ bindings for ",[143,8801,8802],{},"better-sqlite3",". It aggressively stripped node_modules during the ",[143,8805,8806],{},"ASAR"," creation, leaving the production app without a working database.",[10,8809,8810,8811,8816,8817,8820,8821,8823],{},"After checking ",[21,8812,8815],{"href":8813,"rel":8814},"https://github.com/electron/forge/issues",[25],"github issues",", I found the solution: you have to use a specific regex in the ",[143,8818,8819],{},"forge.config.ts"," ignore field to forcefully prevent Forge from discarding ",[143,8822,8802],{}," and its bindings:",[135,8825,8827],{"className":137,"code":8826,"filename":8819,"language":140,"meta":141,"style":141},"packagerConfig: {\n    name: 'Solana Bots',\n    asar: true,\n    ignore: [\n        // force electron Forge to KEEP better-sqlite3 and its bindings\n        /node_modules\\/(?!(better-sqlite3|bindings|file-uri-to-path)\\/)/,\n        /^\\/src($|\\/)/,\n        // ...\n    ]\n}\n",[143,8828,8829,8837,8848,8859,8867,8872,8907,8930,8935,8940],{"__ignoreMap":141},[146,8830,8831,8834],{"class":148,"line":149},[146,8832,8833],{"class":208},"packagerConfig",[146,8835,8836],{"class":156},": {\n",[146,8838,8839,8842,8844,8846],{"class":148,"line":170},[146,8840,8841],{"class":208},"    name",[146,8843,2947],{"class":156},[146,8845,5699],{"class":163},[146,8847,435],{"class":156},[146,8849,8850,8853,8855,8857],{"class":148,"line":185},[146,8851,8852],{"class":208},"    asar",[146,8854,2947],{"class":156},[146,8856,250],{"class":201},[146,8858,435],{"class":156},[146,8860,8861,8864],{"class":148,"line":192},[146,8862,8863],{"class":208},"    ignore",[146,8865,8866],{"class":156},": [\n",[146,8868,8869],{"class":148,"line":221},[146,8870,8871],{"class":2896},"        // force electron Forge to KEEP better-sqlite3 and its bindings\n",[146,8873,8874,8877,8881,8885,8888,8890,8893,8895,8898,8900,8902,8905],{"class":148,"line":256},[146,8875,8876],{"class":163},"        /",[146,8878,8880],{"class":8879},"sQeJH","node_modules",[146,8882,8884],{"class":8883},"sAxt1","\\/",[146,8886,8887],{"class":8879},"(?!(better-sqlite3",[146,8889,1840],{"class":152},[146,8891,8892],{"class":8879},"bindings",[146,8894,1840],{"class":152},[146,8896,8897],{"class":8879},"file-uri-to-path)",[146,8899,8884],{"class":8883},[146,8901,518],{"class":8879},[146,8903,8904],{"class":163},"/",[146,8906,435],{"class":156},[146,8908,8909,8911,8914,8916,8919,8922,8924,8926,8928],{"class":148,"line":279},[146,8910,8876],{"class":163},[146,8912,8913],{"class":152},"^",[146,8915,8884],{"class":8883},[146,8917,8918],{"class":8879},"src(",[146,8920,8921],{"class":152},"$|",[146,8923,8884],{"class":8883},[146,8925,518],{"class":8879},[146,8927,8904],{"class":163},[146,8929,435],{"class":156},[146,8931,8932],{"class":148,"line":300},[146,8933,8934],{"class":2896},"        // ...\n",[146,8936,8937],{"class":148,"line":321},[146,8938,8939],{"class":156},"    ]\n",[146,8941,8942],{"class":148,"line":355},[146,8943,1507],{"class":156},[10,8945,8946,8947,8949,8950,8952,8953,8956,8957,8960,8961,33],{},"To make matters funnier, I found out that the latest versions of ",[143,8948,8802],{}," clash with Electron's internal Chromium/OpenGL APIs on certain operating systems, causing hard crashes. The fix for me was rolling back ",[143,8951,8802],{}," to version ",[143,8954,8955],{},"12.9.0"," and pinning Electron to version ",[143,8958,8959],{},"41.0.0"," instead of the latest ",[143,8962,8963],{},"42.2.0",[375,8965,8967],{"id":8966},"cicd-inno-setup-and-cloudflare-r2","CI/CD, Inno Setup, and Cloudflare R2",[10,8969,8970,8971,8973],{},"Because ",[143,8972,8802],{}," relies on native binaries, you can't build the Windows installer on a Mac/Linux or vice versa. It must be compiled on the target OS. I moved the entire build pipeline into GitHub Actions, starting up both an Ubuntu and Windows runner.",[10,8975,8976,8977,8982],{},"For Windows, Electron's default Maker (Squirrel) is extremely poor for UX, it doesn't let users choose install directories and feels crappy. So I deleted it and switched to ",[21,8978,8981],{"href":8979,"rel":8980},"https://jrsoftware.org/ishelp/",[25],"Inno Setup"," which I later discovered.",[10,8984,8985,8986,8989,8990,33],{},"I wanted the installer to include both the Electron Desktop App and the compiled CLI tool, alongside those migration files. And I wanted the CLI tool to automatically be added to the user's system ",[143,8987,8988],{},"PATH"," so they could just open their terminal and type ",[143,8991,8992],{},"solana-bots",[10,8994,8995,8996,8999,9000,9002],{},"But modifying the system registry is risky; an installer shouldn't just inject environment variables, it needs to clean up after itself when uninstalled. With some help from AI, I wrote a custom Pascal script inside ",[143,8997,8998],{},"installer.iss"," that safely adds the CLI to the ",[143,9001,8988],{},", broadcasts the change to the OS, and parses and removes only that specific path string during uninstallation:",[135,9004,9008],{"className":9005,"code":9006,"language":9007,"meta":141,"style":141},"language-pascal shiki shiki-themes github-light github-dark github-dark","\n[Files]\nSource: \"ui\\out\\Solana Bots-win32-x64\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\nSource: \"build\\cli\\solana-bots-win.exe\"; DestDir: \"{app}\\cli\"; DestName: \"solana-bots.exe\"; Flags: ignoreversion\nSource: \"build\\migrations\\*\"; DestDir: \"{app}\\migrations\"; Flags: ignoreversion recursesubdirs\n\n[Code]\nconst\n    MY_HWND_BROADCAST = $FFFF;\n    MY_WM_SETTINGCHANGE = $001A;\n    MY_SMTO_ABORTIFHUNG = 2;\n\nfunction SendMessageTimeout(hWnd: LongInt; Msg: LongInt; wParam: LongInt; lParam: String; fuFlags: LongInt; uTimeout: LongInt; var lpdwResult: LongInt): LongInt;\n    external 'SendMessageTimeoutW@user32.dll stdcall';\n\nprocedure RefreshEnvironment;\nvar\n    Dummy: LongInt;\nbegin\n    SendMessageTimeout(MY_HWND_BROADCAST, MY_WM_SETTINGCHANGE, 0, 'Environment', MY_SMTO_ABORTIFHUNG, 5000, Dummy);\nend;\n\nprocedure AddToPath;\nvar\n    OldPath: String;\n    NewPath: String;\n    AppPath: String;\nbegin\n    AppPath := ExpandConstant('{app}\\cli'); \n    if RegQueryStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', OldPath) then\n    begin\n        if Pos(Lowercase(AppPath), Lowercase(OldPath)) = 0 then\n        begin\n            NewPath := OldPath + ';' + AppPath;\n            RegWriteExpandStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', NewPath);\n        end;\n    end;\nend;\n\nprocedure CurStepChanged(CurStep: TSetupStep);\nbegin\n    if CurStep = ssPostInstall then\n    begin\n        AddToPath();\n        RefreshEnvironment();\n    end;\nend;\n\nprocedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);\nvar\n    sCurrent: String;\n    sAppPath: String;\n    sCurLower: String;\n    sAppLower: String;\n    p, startIdx, endIdx: Integer;\nbegin\n    if CurUninstallStep = usPostUninstall then\n    begin\n        sAppPath := ExpandConstant('{app}\\cli'); \n        if RegQueryStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', sCurrent) then\n        begin\n            sCurLower := Lowercase(sCurrent);\n            sAppLower := Lowercase(sAppPath);\n            p := Pos(sAppLower, sCurLower);\n            \n            // Cleanly parse and remove the specific CLI path\n            while p > 0 do\n            begin\n                startIdx := p;\n                endIdx := p + Length(sAppLower) - 1;\n                if (startIdx > 1) and (sCurLower[startIdx-1] = ';') then\n                    startIdx := startIdx - 1;\n                if (endIdx \u003C Length(sCurLower)) and (sCurLower[endIdx+1] = ';') then\n                    endIdx := endIdx + 1;\n                Delete(sCurrent, startIdx, endIdx - startIdx + 1);\n                sCurLower := Lowercase(sCurrent);\n                p := Pos(sAppLower, sCurLower);\n            end;\n\n            // Clean up trailing/double semicolons\n            while Pos(';;', sCurrent) > 0 do\n                Delete(sCurrent, Pos(';;', sCurrent), 1);\n            while (Length(sCurrent) > 0) and (sCurrent[1] = ';') do\n                Delete(sCurrent, 1, 1);\n            while (Length(sCurrent) > 0) and (sCurrent[Length(sCurrent)] = ';') do\n                Delete(sCurrent, Length(sCurrent), 1);\n\n            if RegWriteExpandStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', sCurrent) then\n            begin\n                RefreshEnvironment();\n            end;\n        end;\n    end;\nend;\n","pascal",[143,9009,9010,9014,9019,9042,9057,9072,9076,9081,9086,9096,9106,9115,9119,9175,9185,9189,9199,9204,9213,9218,9238,9245,9249,9258,9262,9271,9280,9289,9293,9303,9324,9329,9341,9346,9357,9371,9378,9385,9391,9395,9405,9409,9418,9422,9427,9432,9438,9444,9448,9458,9462,9471,9480,9489,9498,9508,9512,9521,9525,9534,9551,9555,9560,9565,9570,9575,9580,9593,9598,9603,9612,9640,9649,9671,9680,9689,9694,9699,9706,9710,9715,9732,9746,9773,9786,9807,9817,9822,9840,9845,9851,9858,9865,9872],{"__ignoreMap":141},[146,9011,9012],{"class":148,"line":149},[146,9013,189],{"emptyLinePlaceholder":188},[146,9015,9016],{"class":148,"line":170},[146,9017,9018],{"class":156},"[Files]\n",[146,9020,9021,9024,9027,9030,9033,9036,9039],{"class":148,"line":185},[146,9022,9023],{"class":156},"Source: \"ui\\",[146,9025,9026],{"class":152},"out",[146,9028,9029],{"class":156},"\\Solana Bots-win32-x64\\*\"; DestDir: \"",[146,9031,9032],{"class":2896},"{app}",[146,9034,9035],{"class":156},"\"; ",[146,9037,9038],{"class":152},"Flags",[146,9040,9041],{"class":156},": ignoreversion recursesubdirs createallsubdirs\n",[146,9043,9044,9047,9049,9052,9054],{"class":148,"line":192},[146,9045,9046],{"class":156},"Source: \"build\\cli\\solana-bots-win.exe\"; DestDir: \"",[146,9048,9032],{"class":2896},[146,9050,9051],{"class":156},"\\cli\"; DestName: \"solana-bots.exe\"; ",[146,9053,9038],{"class":152},[146,9055,9056],{"class":156},": ignoreversion\n",[146,9058,9059,9062,9064,9067,9069],{"class":148,"line":221},[146,9060,9061],{"class":156},"Source: \"build\\migrations\\*\"; DestDir: \"",[146,9063,9032],{"class":2896},[146,9065,9066],{"class":156},"\\migrations\"; ",[146,9068,9038],{"class":152},[146,9070,9071],{"class":156},": ignoreversion recursesubdirs\n",[146,9073,9074],{"class":148,"line":256},[146,9075,189],{"emptyLinePlaceholder":188},[146,9077,9078],{"class":148,"line":279},[146,9079,9080],{"class":156},"[Code]\n",[146,9082,9083],{"class":148,"line":300},[146,9084,9085],{"class":152},"const\n",[146,9087,9088,9091,9094],{"class":148,"line":321},[146,9089,9090],{"class":156},"    MY_HWND_BROADCAST = ",[146,9092,9093],{"class":201},"$FFFF",[146,9095,167],{"class":156},[146,9097,9098,9101,9104],{"class":148,"line":355},[146,9099,9100],{"class":156},"    MY_WM_SETTINGCHANGE = ",[146,9102,9103],{"class":201},"$001A",[146,9105,167],{"class":156},[146,9107,9108,9111,9113],{"class":148,"line":562},[146,9109,9110],{"class":156},"    MY_SMTO_ABORTIFHUNG = ",[146,9112,2648],{"class":201},[146,9114,167],{"class":156},[146,9116,9117],{"class":148,"line":584},[146,9118,189],{"emptyLinePlaceholder":188},[146,9120,9121,9124,9127,9130,9133,9136,9138,9141,9143,9146,9148,9151,9153,9156,9158,9160,9163,9166,9168,9171,9173],{"class":148,"line":612},[146,9122,9123],{"class":152},"function",[146,9125,9126],{"class":208}," SendMessageTimeout",[146,9128,9129],{"class":156},"(hWnd: ",[146,9131,9132],{"class":152},"LongInt",[146,9134,9135],{"class":156},"; Msg: ",[146,9137,9132],{"class":152},[146,9139,9140],{"class":156},"; wParam: ",[146,9142,9132],{"class":152},[146,9144,9145],{"class":156},"; lParam: ",[146,9147,2920],{"class":152},[146,9149,9150],{"class":156},"; fuFlags: ",[146,9152,9132],{"class":152},[146,9154,9155],{"class":156},"; uTimeout: ",[146,9157,9132],{"class":152},[146,9159,7463],{"class":156},[146,9161,9162],{"class":152},"var",[146,9164,9165],{"class":156}," lpdwResult: ",[146,9167,9132],{"class":152},[146,9169,9170],{"class":156},"): ",[146,9172,9132],{"class":152},[146,9174,167],{"class":156},[146,9176,9177,9180,9183],{"class":148,"line":618},[146,9178,9179],{"class":152},"    external",[146,9181,9182],{"class":163}," 'SendMessageTimeoutW@user32.dll stdcall'",[146,9184,167],{"class":156},[146,9186,9187],{"class":148,"line":641},[146,9188,189],{"emptyLinePlaceholder":188},[146,9190,9191,9194,9197],{"class":148,"line":672},[146,9192,9193],{"class":152},"procedure",[146,9195,9196],{"class":208}," RefreshEnvironment",[146,9198,167],{"class":156},[146,9200,9201],{"class":148,"line":691},[146,9202,9203],{"class":152},"var\n",[146,9205,9206,9209,9211],{"class":148,"line":696},[146,9207,9208],{"class":156},"    Dummy: ",[146,9210,9132],{"class":152},[146,9212,167],{"class":156},[146,9214,9215],{"class":148,"line":709},[146,9216,9217],{"class":152},"begin\n",[146,9219,9220,9223,9225,9227,9230,9233,9235],{"class":148,"line":721},[146,9221,9222],{"class":156},"    SendMessageTimeout(MY_HWND_BROADCAST, MY_WM_SETTINGCHANGE, ",[146,9224,5197],{"class":201},[146,9226,507],{"class":156},[146,9228,9229],{"class":163},"'Environment'",[146,9231,9232],{"class":156},", MY_SMTO_ABORTIFHUNG, ",[146,9234,4204],{"class":201},[146,9236,9237],{"class":156},", Dummy);\n",[146,9239,9240,9243],{"class":148,"line":758},[146,9241,9242],{"class":152},"end",[146,9244,167],{"class":156},[146,9246,9247],{"class":148,"line":764},[146,9248,189],{"emptyLinePlaceholder":188},[146,9250,9251,9253,9256],{"class":148,"line":769},[146,9252,9193],{"class":152},[146,9254,9255],{"class":208}," AddToPath",[146,9257,167],{"class":156},[146,9259,9260],{"class":148,"line":774},[146,9261,9203],{"class":152},[146,9263,9264,9267,9269],{"class":148,"line":813},[146,9265,9266],{"class":156},"    OldPath: ",[146,9268,2920],{"class":152},[146,9270,167],{"class":156},[146,9272,9273,9276,9278],{"class":148,"line":820},[146,9274,9275],{"class":156},"    NewPath: ",[146,9277,2920],{"class":152},[146,9279,167],{"class":156},[146,9281,9282,9285,9287],{"class":148,"line":825},[146,9283,9284],{"class":156},"    AppPath: ",[146,9286,2920],{"class":152},[146,9288,167],{"class":156},[146,9290,9291],{"class":148,"line":840},[146,9292,9217],{"class":152},[146,9294,9295,9298,9301],{"class":148,"line":856},[146,9296,9297],{"class":156},"    AppPath := ExpandConstant(",[146,9299,9300],{"class":163},"'{app}\\cli'",[146,9302,7121],{"class":156},[146,9304,9305,9307,9310,9313,9315,9318,9321],{"class":148,"line":871},[146,9306,903],{"class":152},[146,9308,9309],{"class":156}," RegQueryStringValue(HKLM, ",[146,9311,9312],{"class":163},"'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment'",[146,9314,507],{"class":156},[146,9316,9317],{"class":163},"'Path'",[146,9319,9320],{"class":156},", OldPath) ",[146,9322,9323],{"class":152},"then\n",[146,9325,9326],{"class":148,"line":895},[146,9327,9328],{"class":152},"    begin\n",[146,9330,9331,9333,9336,9338],{"class":148,"line":900},[146,9332,7449],{"class":152},[146,9334,9335],{"class":156}," Pos(Lowercase(AppPath), Lowercase(OldPath)) = ",[146,9337,5197],{"class":201},[146,9339,9340],{"class":152}," then\n",[146,9342,9343],{"class":148,"line":918},[146,9344,9345],{"class":152},"        begin\n",[146,9347,9348,9351,9354],{"class":148,"line":935},[146,9349,9350],{"class":156},"            NewPath := OldPath + ",[146,9352,9353],{"class":163},"';'",[146,9355,9356],{"class":156}," + AppPath;\n",[146,9358,9359,9362,9364,9366,9368],{"class":148,"line":941},[146,9360,9361],{"class":156},"            RegWriteExpandStringValue(HKLM, ",[146,9363,9312],{"class":163},[146,9365,507],{"class":156},[146,9367,9317],{"class":163},[146,9369,9370],{"class":156},", NewPath);\n",[146,9372,9373,9376],{"class":148,"line":946},[146,9374,9375],{"class":152},"        end",[146,9377,167],{"class":156},[146,9379,9380,9383],{"class":148,"line":963},[146,9381,9382],{"class":152},"    end",[146,9384,167],{"class":156},[146,9386,9387,9389],{"class":148,"line":987},[146,9388,9242],{"class":152},[146,9390,167],{"class":156},[146,9392,9393],{"class":148,"line":1007},[146,9394,189],{"emptyLinePlaceholder":188},[146,9396,9397,9399,9402],{"class":148,"line":1027},[146,9398,9193],{"class":152},[146,9400,9401],{"class":208}," CurStepChanged",[146,9403,9404],{"class":156},"(CurStep: TSetupStep);\n",[146,9406,9407],{"class":148,"line":1044},[146,9408,9217],{"class":152},[146,9410,9411,9413,9416],{"class":148,"line":1049},[146,9412,903],{"class":152},[146,9414,9415],{"class":156}," CurStep = ssPostInstall ",[146,9417,9323],{"class":152},[146,9419,9420],{"class":148,"line":1072},[146,9421,9328],{"class":152},[146,9423,9424],{"class":148,"line":1093},[146,9425,9426],{"class":156},"        AddToPath();\n",[146,9428,9429],{"class":148,"line":1105},[146,9430,9431],{"class":156},"        RefreshEnvironment();\n",[146,9433,9434,9436],{"class":148,"line":1110},[146,9435,9382],{"class":152},[146,9437,167],{"class":156},[146,9439,9440,9442],{"class":148,"line":1127},[146,9441,9242],{"class":152},[146,9443,167],{"class":156},[146,9445,9446],{"class":148,"line":1138},[146,9447,189],{"emptyLinePlaceholder":188},[146,9449,9450,9452,9455],{"class":148,"line":1148},[146,9451,9193],{"class":152},[146,9453,9454],{"class":208}," CurUninstallStepChanged",[146,9456,9457],{"class":156},"(CurUninstallStep: TUninstallStep);\n",[146,9459,9460],{"class":148,"line":1154},[146,9461,9203],{"class":152},[146,9463,9464,9467,9469],{"class":148,"line":1174},[146,9465,9466],{"class":156},"    sCurrent: ",[146,9468,2920],{"class":152},[146,9470,167],{"class":156},[146,9472,9473,9476,9478],{"class":148,"line":1204},[146,9474,9475],{"class":156},"    sAppPath: ",[146,9477,2920],{"class":152},[146,9479,167],{"class":156},[146,9481,9482,9485,9487],{"class":148,"line":1231},[146,9483,9484],{"class":156},"    sCurLower: ",[146,9486,2920],{"class":152},[146,9488,167],{"class":156},[146,9490,9491,9494,9496],{"class":148,"line":1267},[146,9492,9493],{"class":156},"    sAppLower: ",[146,9495,2920],{"class":152},[146,9497,167],{"class":156},[146,9499,9500,9503,9506],{"class":148,"line":1283},[146,9501,9502],{"class":156},"    p, startIdx, endIdx: ",[146,9504,9505],{"class":152},"Integer",[146,9507,167],{"class":156},[146,9509,9510],{"class":148,"line":1288},[146,9511,9217],{"class":152},[146,9513,9514,9516,9519],{"class":148,"line":1304},[146,9515,903],{"class":152},[146,9517,9518],{"class":156}," CurUninstallStep = usPostUninstall ",[146,9520,9323],{"class":152},[146,9522,9523],{"class":148,"line":1312},[146,9524,9328],{"class":152},[146,9526,9527,9530,9532],{"class":148,"line":1317},[146,9528,9529],{"class":156},"        sAppPath := ExpandConstant(",[146,9531,9300],{"class":163},[146,9533,7121],{"class":156},[146,9535,9536,9538,9540,9542,9544,9546,9549],{"class":148,"line":5221},[146,9537,7449],{"class":152},[146,9539,9309],{"class":156},[146,9541,9312],{"class":163},[146,9543,507],{"class":156},[146,9545,9317],{"class":163},[146,9547,9548],{"class":156},", sCurrent) ",[146,9550,9323],{"class":152},[146,9552,9553],{"class":148,"line":5229},[146,9554,9345],{"class":152},[146,9556,9557],{"class":148,"line":5239},[146,9558,9559],{"class":156},"            sCurLower := Lowercase(sCurrent);\n",[146,9561,9562],{"class":148,"line":5244},[146,9563,9564],{"class":156},"            sAppLower := Lowercase(sAppPath);\n",[146,9566,9567],{"class":148,"line":5262},[146,9568,9569],{"class":156},"            p := Pos(sAppLower, sCurLower);\n",[146,9571,9572],{"class":148,"line":5283},[146,9573,9574],{"class":156},"            \n",[146,9576,9577],{"class":148,"line":5293},[146,9578,9579],{"class":2896},"            // Cleanly parse and remove the specific CLI path\n",[146,9581,9582,9585,9588,9590],{"class":148,"line":5303},[146,9583,9584],{"class":152},"            while",[146,9586,9587],{"class":156}," p > ",[146,9589,5197],{"class":201},[146,9591,9592],{"class":152}," do\n",[146,9594,9595],{"class":148,"line":5309},[146,9596,9597],{"class":152},"            begin\n",[146,9599,9600],{"class":148,"line":5317},[146,9601,9602],{"class":156},"                startIdx := p;\n",[146,9604,9605,9608,9610],{"class":148,"line":5327},[146,9606,9607],{"class":156},"                endIdx := p + Length(sAppLower) - ",[146,9609,464],{"class":201},[146,9611,167],{"class":156},[146,9613,9614,9617,9620,9622,9624,9626,9629,9631,9634,9636,9638],{"class":148,"line":5333},[146,9615,9616],{"class":152},"                if",[146,9618,9619],{"class":156}," (startIdx > ",[146,9621,464],{"class":201},[146,9623,1876],{"class":156},[146,9625,3705],{"class":152},[146,9627,9628],{"class":156}," (sCurLower[startIdx-",[146,9630,464],{"class":201},[146,9632,9633],{"class":156},"] = ",[146,9635,9353],{"class":163},[146,9637,1876],{"class":156},[146,9639,9323],{"class":152},[146,9641,9642,9645,9647],{"class":148,"line":5339},[146,9643,9644],{"class":156},"                    startIdx := startIdx - ",[146,9646,464],{"class":201},[146,9648,167],{"class":156},[146,9650,9651,9653,9656,9658,9661,9663,9665,9667,9669],{"class":148,"line":5345},[146,9652,9616],{"class":152},[146,9654,9655],{"class":156}," (endIdx \u003C Length(sCurLower)) ",[146,9657,3705],{"class":152},[146,9659,9660],{"class":156}," (sCurLower[endIdx+",[146,9662,464],{"class":201},[146,9664,9633],{"class":156},[146,9666,9353],{"class":163},[146,9668,1876],{"class":156},[146,9670,9323],{"class":152},[146,9672,9673,9676,9678],{"class":148,"line":5351},[146,9674,9675],{"class":156},"                    endIdx := endIdx + ",[146,9677,464],{"class":201},[146,9679,167],{"class":156},[146,9681,9682,9685,9687],{"class":148,"line":5357},[146,9683,9684],{"class":156},"                Delete(sCurrent, startIdx, endIdx - startIdx + ",[146,9686,464],{"class":201},[146,9688,559],{"class":156},[146,9690,9691],{"class":148,"line":5369},[146,9692,9693],{"class":156},"                sCurLower := Lowercase(sCurrent);\n",[146,9695,9696],{"class":148,"line":5375},[146,9697,9698],{"class":156},"                p := Pos(sAppLower, sCurLower);\n",[146,9700,9701,9704],{"class":148,"line":5380},[146,9702,9703],{"class":152},"            end",[146,9705,167],{"class":156},[146,9707,9708],{"class":148,"line":5385},[146,9709,189],{"emptyLinePlaceholder":188},[146,9711,9712],{"class":148,"line":5395},[146,9713,9714],{"class":2896},"            // Clean up trailing/double semicolons\n",[146,9716,9717,9719,9722,9725,9728,9730],{"class":148,"line":5400},[146,9718,9584],{"class":152},[146,9720,9721],{"class":156}," Pos(",[146,9723,9724],{"class":163},"';;'",[146,9726,9727],{"class":156},", sCurrent) > ",[146,9729,5197],{"class":201},[146,9731,9592],{"class":152},[146,9733,9734,9737,9739,9742,9744],{"class":148,"line":5419},[146,9735,9736],{"class":156},"                Delete(sCurrent, Pos(",[146,9738,9724],{"class":163},[146,9740,9741],{"class":156},", sCurrent), ",[146,9743,464],{"class":201},[146,9745,559],{"class":156},[146,9747,9748,9750,9753,9755,9757,9759,9762,9764,9766,9768,9770],{"class":148,"line":5440},[146,9749,9584],{"class":152},[146,9751,9752],{"class":156}," (Length(sCurrent) > ",[146,9754,5197],{"class":201},[146,9756,1876],{"class":156},[146,9758,3705],{"class":152},[146,9760,9761],{"class":156}," (sCurrent[",[146,9763,464],{"class":201},[146,9765,9633],{"class":156},[146,9767,9353],{"class":163},[146,9769,1876],{"class":156},[146,9771,9772],{"class":152},"do\n",[146,9774,9775,9778,9780,9782,9784],{"class":148,"line":5445},[146,9776,9777],{"class":156},"                Delete(sCurrent, ",[146,9779,464],{"class":201},[146,9781,507],{"class":156},[146,9783,464],{"class":201},[146,9785,559],{"class":156},[146,9787,9788,9790,9792,9794,9796,9798,9801,9803,9805],{"class":148,"line":5451},[146,9789,9584],{"class":152},[146,9791,9752],{"class":156},[146,9793,5197],{"class":201},[146,9795,1876],{"class":156},[146,9797,3705],{"class":152},[146,9799,9800],{"class":156}," (sCurrent[Length(sCurrent)] = ",[146,9802,9353],{"class":163},[146,9804,1876],{"class":156},[146,9806,9772],{"class":152},[146,9808,9810,9813,9815],{"class":148,"line":9809},86,[146,9811,9812],{"class":156},"                Delete(sCurrent, Length(sCurrent), ",[146,9814,464],{"class":201},[146,9816,559],{"class":156},[146,9818,9820],{"class":148,"line":9819},87,[146,9821,189],{"emptyLinePlaceholder":188},[146,9823,9825,9827,9830,9832,9834,9836,9838],{"class":148,"line":9824},88,[146,9826,1849],{"class":152},[146,9828,9829],{"class":156}," RegWriteExpandStringValue(HKLM, ",[146,9831,9312],{"class":163},[146,9833,507],{"class":156},[146,9835,9317],{"class":163},[146,9837,9548],{"class":156},[146,9839,9323],{"class":152},[146,9841,9843],{"class":148,"line":9842},89,[146,9844,9597],{"class":152},[146,9846,9848],{"class":148,"line":9847},90,[146,9849,9850],{"class":156},"                RefreshEnvironment();\n",[146,9852,9854,9856],{"class":148,"line":9853},91,[146,9855,9703],{"class":152},[146,9857,167],{"class":156},[146,9859,9861,9863],{"class":148,"line":9860},92,[146,9862,9375],{"class":152},[146,9864,167],{"class":156},[146,9866,9868,9870],{"class":148,"line":9867},93,[146,9869,9382],{"class":152},[146,9871,167],{"class":156},[146,9873,9875,9877],{"class":148,"line":9874},94,[146,9876,9242],{"class":152},[146,9878,167],{"class":156},[10,9880,9881,9882,9889],{},"Finally, rather than using standard GitHub releases for distribution, I wanted to host the binaries myself to control the update flow. In the GitHub Actions workflow, I used ",[21,9883,9886],{"href":9884,"rel":9885},"https://rclone.org/",[25],[143,9887,9888],{},"rclone"," to automatically push the compiled artifacts directly to a Cloudflare R2 bucket, alongside a Bash script that parses the directory names to manage versioning:",[135,9891,9895],{"className":9892,"code":9893,"language":9894,"meta":141,"style":141},"language-yml shiki shiki-themes github-light github-dark github-dark","- name: Upload to R2\n        env:\n          CLOUDFLARE_R2_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}\n          # ...\n        run: |\n          VERSION_STR=\"${{ needs.build-linux.outputs.version }}\"\n          R2_PREFIX=\"releases/${VERSION_STR}\"\n\n          curl -fsSL https://rclone.org/install.sh | sudo bash\n          \n          # ... rclone configuration ...\n          \n          rclone copy build/ \"r2:${CLOUDFLARE_R2_BUCKET}/${R2_PREFIX}\" --progress --no-traverse\n","yml",[143,9896,9897,9911,9918,9928,9933,9943,9948,9953,9957,9962,9967,9972,9976],{"__ignoreMap":141},[146,9898,9899,9902,9906,9908],{"class":148,"line":149},[146,9900,9901],{"class":156},"- ",[146,9903,9905],{"class":9904},"sByVh","name",[146,9907,2947],{"class":156},[146,9909,9910],{"class":163},"Upload to R2\n",[146,9912,9913,9916],{"class":148,"line":170},[146,9914,9915],{"class":9904},"        env",[146,9917,6590],{"class":156},[146,9919,9920,9923,9925],{"class":148,"line":185},[146,9921,9922],{"class":9904},"          CLOUDFLARE_R2_ACCESS_KEY_ID",[146,9924,2947],{"class":156},[146,9926,9927],{"class":163},"${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}\n",[146,9929,9930],{"class":148,"line":192},[146,9931,9932],{"class":2896},"          # ...\n",[146,9934,9935,9938,9940],{"class":148,"line":221},[146,9936,9937],{"class":9904},"        run",[146,9939,2947],{"class":156},[146,9941,9942],{"class":152},"|\n",[146,9944,9945],{"class":148,"line":256},[146,9946,9947],{"class":163},"          VERSION_STR=\"${{ needs.build-linux.outputs.version }}\"\n",[146,9949,9950],{"class":148,"line":279},[146,9951,9952],{"class":163},"          R2_PREFIX=\"releases/${VERSION_STR}\"\n",[146,9954,9955],{"class":148,"line":300},[146,9956,189],{"emptyLinePlaceholder":188},[146,9958,9959],{"class":148,"line":321},[146,9960,9961],{"class":163},"          curl -fsSL https://rclone.org/install.sh | sudo bash\n",[146,9963,9964],{"class":148,"line":355},[146,9965,9966],{"class":163},"          \n",[146,9968,9969],{"class":148,"line":562},[146,9970,9971],{"class":163},"          # ... rclone configuration ...\n",[146,9973,9974],{"class":148,"line":584},[146,9975,9966],{"class":163},[146,9977,9978],{"class":148,"line":612},[146,9979,9980],{"class":163},"          rclone copy build/ \"r2:${CLOUDFLARE_R2_BUCKET}/${R2_PREFIX}\" --progress --no-traverse\n",[35,9982,9983],{"id":4562},"Finally",[10,9985,9986],{},"What started as a favor for a friend evolved into a full-scale application bridging the gap between low-level blockchain cryptography, desktop frameworks, and automated deployment pipelines. It was an absolute headache to piece together, but an incredibly awesome learning experience.",[10,9988,9989,9990,9994,9995,9999],{},"Give it a try, you can download it from its ",[21,9991,26],{"href":9992,"rel":9993},"https://solana.riavzon.com/download",[25],", and send me ",[21,9996,9998],{"href":9997},"/contact","feedback"," later.",[10001,10002,10003],"style",{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sQeJH, html code.shiki .sQeJH{--shiki-light:#032F62;--shiki-default:#DBEDFF;--shiki-dark:#DBEDFF}html pre.shiki code .sAxt1, html code.shiki .sAxt1{--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-default:#85E89D;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sByVh, html code.shiki .sByVh{--shiki-light:#22863A;--shiki-default:#85E89D;--shiki-dark:#85E89D}",{"title":141,"searchDepth":170,"depth":170,"links":10005},[10006,10007,10011,10018],{"id":37,"depth":170,"text":38},{"id":117,"depth":170,"text":118,"children":10008},[10009,10010],{"id":377,"depth":185,"text":378},{"id":2230,"depth":185,"text":2231},{"id":5925,"depth":170,"text":5926,"children":10012},[10013,10014,10015,10016,10017],{"id":7214,"depth":185,"text":7215},{"id":8133,"depth":185,"text":8134},{"id":8155,"depth":185,"text":8156},{"id":8628,"depth":185,"text":8629},{"id":8966,"depth":185,"text":8967},{"id":4562,"depth":170,"text":9983},"26/06/2026","A deep dive into the technical realities of building Electron applications for the Solana Blockchain, from encryption, states to handling concurrent network operations.","md",null,{},"/articles/the-electron-experience","20",{"title":5,"description":10020},{"loc":10024},"articles/00.the-electron-experience",[10030,10031,10032],"Electron","Desktop","Crypto","xlDG0aDY99DwEn0lNef3qQ4L3ttmKWDb-6hZUCfW5Ug",1779819065223]