diff --git a/package.json b/package.json index 508b751e0..8d342c881 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "binance-api-node": "^0.12.7", "bs58": "^5.0.0", "cross-fetch": "^3.1.5", + "csv-parse": "^5.5.6", "dotenv": "^16.0.3", "fast-copy": "^3.0.1", "lodash": "^4.17.21", diff --git a/ts/client/scripts/reimbursement.ts b/ts/client/scripts/reimbursement.ts new file mode 100644 index 000000000..c892b5140 --- /dev/null +++ b/ts/client/scripts/reimbursement.ts @@ -0,0 +1,231 @@ +import { Connection, Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; +import fs from 'fs'; +import * as path from 'path'; +import { parse } from 'csv-parse'; +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import { + createComputeBudgetIx, + MANGO_V4_ID, + MangoClient, + toNative, + USDC_MINT, +} from '../src'; +import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'; +import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions'; +import { + SequenceType, + TransactionInstructionWithSigners, +} from '@blockworks-foundation/mangolana/lib/globalTypes'; + +const MANGO_MAINNET_GROUP = new PublicKey( + '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX', +); + +type Reimbursement = { + mango_account: string; + owner: string; + mangoSOL: number; + MOTHER: number; + SOL: number; + USDC: number; + Notional: string; +}; + +const setupWallet = () => { + const user = Keypair.fromSecretKey( + Buffer.from( + JSON.parse( + fs.readFileSync('keypair.json', { + encoding: 'utf-8', + }), + ), + ), + ); + + return user; +}; + +const readCsv = async () => { + const csvFilePath = path.resolve(__dirname, 'reimbursement.csv'); + + const headers = [ + 'mango_account', + 'owner', + 'mangoSOL', + 'MOTHER', + 'SOL', + 'USDC', + 'Notional', + ]; + + return new Promise((resolve, reject) => { + const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' }); + + parse( + fileContent, + { + delimiter: ',', + columns: headers, + }, + (error, result: Reimbursement[]) => { + if (error) { + reject(error); + } else { + const resp = result.slice(1, result.length); + resolve(resp); + } + }, + ); + }); +}; + +const tryGetPubKey = (pubkey: string | string[]) => { + try { + return new PublicKey(pubkey); + } catch (e) { + console.log(e); + return null; + } +}; + +const mints = { + mangoSOL: new PublicKey('MangmsBgFqJhW4cLUR9LxfVgMboY1xAoP8UUBiWwwuY'), + MOTHER: new PublicKey('3S8qX1MsMqRbiwKg2cQyx7nis1oHMgaCuc9c4VfvVdPN'), + SOL: WRAPPED_SOL_MINT, + USDC: USDC_MINT, +}; +const backups = [new Connection(''), new Connection('')]; + +const main = async () => { + const user = await setupWallet(); + const mainConnection = new Connection(''); + const backupConnections = backups; + const options = AnchorProvider.defaultOptions(); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(mainConnection, userWallet, options); + + const client = await MangoClient.connect( + userProvider, + 'mainnet-beta', + MANGO_V4_ID['mainnet-beta'], + { + idsSource: 'api', + multipleConnections: backupConnections, + prioritizationFee: 200000, + }, + ); + console.log(userWallet.publicKey.toBase58(), '@@@@@'); + const group = await client.getGroup(MANGO_MAINNET_GROUP); + + const csvData = await readCsv(); + + const TO_PROCESS = csvData; + const TOKEN = 'SOL'; + + const notReimbursedMangoAccounts: string[] = []; + for (const row of TO_PROCESS) { + const mangoAccountPk = tryGetPubKey(row.mango_account); + + if (mangoAccountPk) { + const mint = mints[TOKEN as keyof typeof mints]; + const amount = Number(row[TOKEN as keyof typeof mints]); + try { + if (mint && amount > 0.0001) { + const decimals = group.getMintDecimals(mint); + const nativeAmount = toNative(amount, decimals); + const mangoAccount = await client.getMangoAccount(mangoAccountPk); + console.log('Mango Account exists'); + console.log( + `Start reimbursing ${mint.toBase58()} ${amount} ${ + row.mango_account + }`, + ); + try { + const signature = await client.tokenDepositNative( + group, + mangoAccount, + mint, + nativeAmount, + false, + true, + ); + + console.log( + 'Reimburse end ', + signature.signature, + signature.confirmationStatus, + signature.err, + ); + if (!signature.err) { + console.log('OK'); + } else { + const ix = SystemProgram.transfer({ + fromPubkey: userWallet.publicKey, + toPubkey: new PublicKey(row.owner), + lamports: toNative(amount, 9).toNumber(), + }); + await sendSignAndConfirmTransactions({ + connection: userProvider.connection, + wallet: userWallet, + transactionInstructions: [ + { + instructionsSet: [ + new TransactionInstructionWithSigners( + createComputeBudgetIx(200000), + ), + new TransactionInstructionWithSigners(ix), + ], + sequenceType: SequenceType.Sequential, + }, + ], + backupConnections: [...backups], + config: { + maxTxesInBatch: 2, + autoRetry: true, + logFlowInfo: true, + }, + }); + } + } catch (e) { + console.log(e); + notReimbursedMangoAccounts.push(row.mango_account); + } + } + } catch (e) { + console.log('Mango account not exists', e); + const ix = SystemProgram.transfer({ + fromPubkey: userWallet.publicKey, + toPubkey: new PublicKey(row.owner), + lamports: toNative(amount, 9).toNumber(), + }); + await sendSignAndConfirmTransactions({ + connection: userProvider.connection, + wallet: userWallet, + transactionInstructions: [ + { + instructionsSet: [ + new TransactionInstructionWithSigners( + createComputeBudgetIx(200000), + ), + new TransactionInstructionWithSigners(ix), + ], + sequenceType: SequenceType.Sequential, + }, + ], + backupConnections: [...backups], + config: { + maxTxesInBatch: 2, + autoRetry: true, + logFlowInfo: true, + }, + }); + } + } else { + console.log('Invalid PublicKey: ', row.mango_account); + throw 'Invalid PublicKey'; + } + } + console.log(notReimbursedMangoAccounts); +}; + +main(); diff --git a/ts/client/scripts/reimbursementSplTokens.ts b/ts/client/scripts/reimbursementSplTokens.ts new file mode 100644 index 000000000..3a0445533 --- /dev/null +++ b/ts/client/scripts/reimbursementSplTokens.ts @@ -0,0 +1,256 @@ +import { + ComputeBudgetProgram, + Connection, + Keypair, + PublicKey, +} from '@solana/web3.js'; +import fs from 'fs'; +import * as path from 'path'; +import { parse } from 'csv-parse'; +import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; +import { + createComputeBudgetIx, + MANGO_V4_ID, + MangoClient, + toNative, + USDC_MINT, +} from '../src'; +import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'; +import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions'; +import { + SequenceType, + TransactionInstructionWithSigners, +} from '@blockworks-foundation/mangolana/lib/globalTypes'; +import { + createAssociatedTokenAccountIdempotentInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, +} from '@solana/spl-token'; + +const MANGO_MAINNET_GROUP = new PublicKey( + '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX', +); + +type Reimbursement = { + mango_account: string; + owner: string; + mangoSOL: number; + MOTHER: number; + SOL: number; + USDC: number; + Notional: string; +}; + +const setupWallet = () => { + const user = Keypair.fromSecretKey( + Buffer.from( + JSON.parse( + fs.readFileSync('keypair.json', { + encoding: 'utf-8', + }), + ), + ), + ); + + return user; +}; + +const readCsv = async () => { + const csvFilePath = path.resolve(__dirname, 'reimbursement.csv'); + + const headers = [ + 'mango_account', + 'owner', + 'mangoSOL', + 'MOTHER', + 'SOL', + 'USDC', + 'Notional', + ]; + + return new Promise((resolve, reject) => { + const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' }); + + parse( + fileContent, + { + delimiter: ',', + columns: headers, + }, + (error, result: Reimbursement[]) => { + if (error) { + reject(error); + } else { + const resp = result.slice(1, result.length); + resolve(resp); + } + }, + ); + }); +}; + +const tryGetPubKey = (pubkey: string | string[]) => { + try { + return new PublicKey(pubkey); + } catch (e) { + console.log(e); + return null; + } +}; + +const sendTokenDeposit = ( + owner: string, + wallet: Wallet, + nativeAmount: BN, + connection: Connection, +) => { + const userAta = getAssociatedTokenAddressSync( + USDC_MINT, + new PublicKey(owner), + true, + ); + const myAta = getAssociatedTokenAddressSync( + USDC_MINT, + wallet.publicKey, + true, + ); + const createAtaIx = createAssociatedTokenAccountIdempotentInstruction( + wallet.publicKey, + userAta, + new PublicKey(owner), + USDC_MINT, + ); + const sendIx = createTransferInstruction( + myAta, + userAta, + wallet.publicKey, + nativeAmount.toNumber(), + ); + return sendSignAndConfirmTransactions({ + connection: connection, + wallet: wallet, + transactionInstructions: [ + { + instructionsSet: [ + new TransactionInstructionWithSigners( + ComputeBudgetProgram.setComputeUnitLimit({ + units: 40000, + }), + ), + new TransactionInstructionWithSigners(createComputeBudgetIx(2000000)), + new TransactionInstructionWithSigners(createAtaIx), + new TransactionInstructionWithSigners(sendIx), + ], + sequenceType: SequenceType.Sequential, + }, + ], + backupConnections: [...backups], + config: { + maxTxesInBatch: 2, + autoRetry: true, + logFlowInfo: true, + }, + }); +}; + +const mints = { + mangoSOL: new PublicKey('MangmsBgFqJhW4cLUR9LxfVgMboY1xAoP8UUBiWwwuY'), + MOTHER: new PublicKey('3S8qX1MsMqRbiwKg2cQyx7nis1oHMgaCuc9c4VfvVdPN'), + SOL: WRAPPED_SOL_MINT, + USDC: USDC_MINT, +}; +const backups = [new Connection(''), new Connection('')]; + +const main = async () => { + const user = await setupWallet(); + const mainConnection = new Connection(''); + const backupConnections = backups; + const options = AnchorProvider.defaultOptions(); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(mainConnection, userWallet, options); + + const client = await MangoClient.connect( + userProvider, + 'mainnet-beta', + MANGO_V4_ID['mainnet-beta'], + { + idsSource: 'api', + multipleConnections: backupConnections, + prioritizationFee: 2000000, + }, + ); + console.log(userWallet.publicKey.toBase58(), '@@@@@'); + const group = await client.getGroup(MANGO_MAINNET_GROUP); + + const csvData = await readCsv(); + + const TO_PROCESS = csvData; + const TOKEN = 'SOL'; + + const notReimbursedMangoAccounts: string[] = []; + for (const row of TO_PROCESS) { + const mangoAccountPk = tryGetPubKey(row.mango_account); + + if (mangoAccountPk) { + const mint = mints[TOKEN as keyof typeof mints]; + const amount = Number(row[TOKEN as keyof typeof mints]); + const decimals = group.getMintDecimals(mint); + const nativeAmount = toNative(amount, decimals); + try { + if (mint && amount > 0.05) { + const mangoAccount = await client.getMangoAccount(mangoAccountPk); + console.log('Mango Account exists'); + console.log( + `Start reimbursing ${mint.toBase58()} ${amount} ${ + row.mango_account + }`, + ); + try { + const signature = await client.tokenDepositNative( + group, + mangoAccount, + mint, + nativeAmount, + false, + true, + ); + + console.log( + 'Reimburse end ', + signature.signature, + signature.confirmationStatus, + signature.err, + ); + if (!signature.err) { + console.log('OK'); + } else { + await sendTokenDeposit( + row.owner, + userWallet, + nativeAmount, + userProvider.connection, + ); + } + } catch (e) { + console.log(e); + notReimbursedMangoAccounts.push(row.mango_account); + } + } + } catch (e) { + console.log('Mango account not exists', e); + await sendTokenDeposit( + row.owner, + userWallet, + nativeAmount, + userProvider.connection, + ); + } + } else { + console.log('Invalid PublicKey: ', row.mango_account); + throw 'Invalid PublicKey'; + } + } + console.log(notReimbursedMangoAccounts); +}; + +main(); diff --git a/yarn.lock b/yarn.lock index 963b02d3b..618680681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1671,6 +1671,11 @@ crypto-hash@^1.3.0: resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== +csv-parse@^5.5.6: + version "5.5.6" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a" + integrity sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A== + debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" @@ -2766,14 +2771,14 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== -node-fetch@3.3.2, "node-fetch@npm:@blockworks-foundation/node-fetch@2.6.11": +node-fetch@3.3.2, node-fetch@^2.6.12, node-fetch@^2.7.0, "node-fetch@npm:@blockworks-foundation/node-fetch@2.6.11": version "2.6.11" resolved "https://registry.yarnpkg.com/@blockworks-foundation/node-fetch/-/node-fetch-2.6.11.tgz#fb536ef0e6a960e7b7993f3c1d3b3bba9bdfbc56" integrity sha512-HeDTxpIypSR4qCoqgUXGr8YL4OG1z7BbV4VhQ9iQs+pt2wV3MtqO+sQk2vXK3WDKu5C6BsbGmWE22BmIrcuOOw== dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.7.0: +node-fetch@^2.6.1: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==