Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reimburse: ensure ata, ensure not transferred in last 10 txs, transfer #118

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"cross-env": "^7.0.2",
"eslint": "^7.28.0",
"eslint-config-prettier": "^7.2.0",
"fast-csv": "^4.3.6",
"mango_risk_check": "^1.2.1",
"mocha": "9",
"prettier": "^2.0.5",
Expand Down
215 changes: 215 additions & 0 deletions src/scripts/reimburse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
} from '@solana/spl-token';
import * as path from 'path';
import * as csv from 'fast-csv';
import {
Commitment,
Connection,
Keypair,
PublicKey,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { Cluster, Config } from '../config';
import fs from 'fs';

const PAYER_KEYPAIR = process.env.MB_PAYER_KEYPAIR;
const PAYER = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
);
const SOURCE = PAYER.publicKey;

const config = Config.ids();
const cluster = 'mainnet' as Cluster;
const connection = new Connection(
config.cluster_urls[cluster],
'confirmed' as Commitment,
);

const groupName = 'mainnet.1';
const groupIds = config.getGroup(cluster, groupName);
if (!groupIds) {
throw new Error(`Group ${groupName} not found`);
}

const mangoProgramId = groupIds.mangoProgramId;
const mangoGroupKey = groupIds.publicKey;
const client = new MangoClient(connection, mangoProgramId);

export async function createAssociatedTokenAccountIdempotentInstruction(
payer: PublicKey,
owner: PublicKey,
mint: PublicKey,
ata: PublicKey,
): Promise<TransactionInstruction> {
return new TransactionInstruction({
keys: [
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: false, isWritable: false },
{ pubkey: mint, isSigner: false, isWritable: false },
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
],
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
data: Buffer.from([0x1]),
});
}

async function tokenTransfer(
mangoAccountOwnerPk: PublicKey,
mint: PublicKey,
nativeTokenAmountToReimburse: BN,
) {
const sourceAta = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint,
SOURCE,
);
const destinationAta = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint,
mangoAccountOwnerPk,
);

// Verify that this tx has not happend in last 100 txs for the destinationAta
const sigs = await connection.getConfirmedSignaturesForAddress2(
destinationAta,
);
for (const sig of sigs.slice(0, 100)) {
const meta = await connection.getParsedTransaction(
sig.signature,
'confirmed',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only looking at a limited set of tx in the past sounds unsafe. I don't know how to do this well though, maybe we store which addresses we've tried to deal with and if we need to rerun we check all tx back to a certain start-slot for our source address and filter out candidates that way?


// Simple check to see if the sourceAta was involved in a tx with destination ata
if (
meta?.transaction.message.accountKeys.find((accountKey) =>
accountKey.pubkey.equals(sourceAta),
)
) {
console.log(` - already transferred`);
return;
}
}

// Build tx
const tx = new Transaction();
tx.add(
await createAssociatedTokenAccountIdempotentInstruction(
PAYER.publicKey,
mangoAccountOwnerPk,
mint,
destinationAta,
),
);
tx.add(
await Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
sourceAta,
await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint,
mangoAccountOwnerPk,
),
PAYER.publicKey,
[PAYER],
nativeTokenAmountToReimburse.toNumber(), // throws `Note: Blob.encode[amount] requires (length 8) Buffer as src` when BN is used
),
);

// Send and confirm
const sig = await sendAndConfirmTransaction(connection, tx, [PAYER], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sending lots and lots of tx is something that's done well in the solana program deploy cli tool. Honestly, I'd borrow that. They know how to send and resend thousands of tx safely and in a timely manner.

skipPreflight: true,
});
console.log(` - transferred, sig https://explorer.solana.com/tx/${sig}`);
}

async function reimburseUser(
mangoAccountOwnerPk: PublicKey,
nativeTokenAmountsToReimburse: BN[],
): Promise<void> {
const group = await client.getMangoGroup(mangoGroupKey);
const allTokens = 16;

// verify input from csv
if (nativeTokenAmountsToReimburse.length !== allTokens) {
throw new Error(
`Mango V3 has ${allTokens} tokens, expected ${allTokens} token amounts to reimburse!`,
);
}

group.tokens.map(async (token, tokenIndex) => {
const tokenConfig = groupIds?.tokens.find((tokenConfig) =>
token.mint.equals(tokenConfig.mintKey),
);

// Token slot empty
if (!tokenConfig) {
return;
}

// Token is deactivated
if (token.oracleInactive) {
return;
}

// Skip if no reimbursements for mint
const nativeTokenAmountToReimburse =
nativeTokenAmountsToReimburse[tokenIndex];
if (nativeTokenAmountToReimburse.eq(new BN(0))) {
return;
}

console.log(
`Transferring ${nativeTokenAmountToReimburse} native ${tokenConfig.symbol} (mint - ${tokenConfig.mintKey}) to ${mangoAccountOwnerPk}`,
);

return await tokenTransfer(
mangoAccountOwnerPk,
token.mint,
nativeTokenAmountToReimburse,
);
});
}

// Example
reimburseUser(new PublicKey('9Ut1gZJnd5D7EjPXm2DygYWZkZGpt5QSMEYAaVx2hur4'), [
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(1), // USDC
]);

// TODO read csv, grab mango accounts owner, grab token deposits per token, call reimburseUser
fs.createReadStream(path.resolve(__dirname, 'assets', 'output.csv'))
.pipe(csv.parse({ headers: true }))
.on('error', (error) => console.error(error))
.on('data', (row) => console.log(row.mango_account, row.equity))
.on('end', (rowCount: number) => console.log(`Parsed ${rowCount} rows`));
78 changes: 78 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@
"@ethersproject/logger" "^5.6.0"
hash.js "1.1.7"

"@fast-csv/[email protected]":
version "4.3.5"
resolved "https://registry.yarnpkg.com/@fast-csv/format/-/format-4.3.5.tgz#90d83d1b47b6aaf67be70d6118f84f3e12ee1ff3"
integrity sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==
dependencies:
"@types/node" "^14.0.1"
lodash.escaperegexp "^4.1.2"
lodash.isboolean "^3.0.3"
lodash.isequal "^4.5.0"
lodash.isfunction "^3.0.9"
lodash.isnil "^4.0.0"

"@fast-csv/[email protected]":
version "4.3.6"
resolved "https://registry.yarnpkg.com/@fast-csv/parse/-/parse-4.3.6.tgz#ee47d0640ca0291034c7aa94039a744cfb019264"
integrity sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==
dependencies:
"@types/node" "^14.0.1"
lodash.escaperegexp "^4.1.2"
lodash.groupby "^4.6.0"
lodash.isfunction "^3.0.9"
lodash.isnil "^4.0.0"
lodash.isundefined "^3.0.1"
lodash.uniq "^4.5.0"

"@hapi/hoek@^9.0.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
Expand Down Expand Up @@ -406,6 +431,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.52.tgz#2fd2dc6bfa185601b15457398d4ba1ef27f81251"
integrity sha512-cfkwWw72849SNYp3Zx0IcIs25vABmFh73xicxhCkTcvtZQeIez15PpwQN8fY3RD7gv1Wrxlc9MEtfMORZDEsGw==

"@types/node@^14.0.1":
version "14.18.32"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.32.tgz#8074f7106731f1a12ba993fe8bad86ee73905014"
integrity sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow==

"@types/node@^15.12.4":
version "15.12.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26"
Expand Down Expand Up @@ -1209,6 +1239,14 @@ eyes@^0.1.8:
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=

fast-csv@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/fast-csv/-/fast-csv-4.3.6.tgz#70349bdd8fe4d66b1130d8c91820b64a21bc4a63"
integrity sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==
dependencies:
"@fast-csv/format" "4.3.5"
"@fast-csv/parse" "4.3.6"

fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
Expand Down Expand Up @@ -1666,6 +1704,41 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=

lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==

lodash.groupby@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1"
integrity sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==

lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==

lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==

lodash.isfunction@^3.0.9:
version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==

lodash.isnil@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c"
integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==

lodash.isundefined@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48"
integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==

lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
Expand All @@ -1676,6 +1749,11 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=

lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==

lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
Expand Down