feat: sponsor tx replacer cli

Signed-off-by: bestmike007 <i@bestmike007.com>
This commit is contained in:
bestmike007
2024-02-15 23:18:37 -06:00
parent f21c0d10d1
commit 13e5ce5cb4
5 changed files with 186 additions and 2 deletions

View File

@@ -39,6 +39,7 @@
"safe-json-stringify": "^1.2.0",
"slonik": "^37.2.0",
"ts-clarity": "^0.0.16",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -49,6 +50,7 @@
"@types/node": "^20.11.18",
"@types/ramda": "^0.29.10",
"@types/safe-json-stringify": "^1.1.5",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"eslint": "^8.56.0",

View File

@@ -0,0 +1,173 @@
import { StacksMainnet, StacksMocknet } from '@stacks/network';
import {
broadcastTransaction,
deserializeTransaction,
sponsorTransaction,
} from '@stacks/transactions';
import { getAccountNonces, getNodeInfo } from 'ts-clarity';
import yargs from 'yargs-parser';
import { getSponsorAccounts } from '../accounts';
import { kStacksEndpoint, kStacksNetworkType } from '../config';
import { UserOperation, getPgPool, sql } from '../db';
import { stringify } from '../util';
// DO NOT USE THIS unless you know what you're doing!
async function main() {
const argv = yargs(process.argv.slice(2), {
configuration: { 'parse-numbers': false },
});
let tx = String(argv.tx);
if (tx.startsWith('\\x') || tx.startsWith('0x')) {
tx = tx.substring(2);
}
if (tx.startsWith('x')) {
tx = tx.substring(1);
}
if (!tx.match(/[0-9a-fA-F]{64}/)) {
console.log(`Invalid tx: ${tx}`);
return;
}
const tx_id = Buffer.from(tx, 'hex');
const sponsorAccount = argv.account;
const nonce = Number(BigInt(argv.nonce));
const gas = BigInt(argv.gas);
const account = getSponsorAccounts().find(a => a.address === sponsorAccount);
if (account == null) {
console.log(`Invalid sponsor account: ${sponsorAccount}`);
return;
}
const { last_executed_tx_nonce } = await getAccountNonces(sponsorAccount, {
stacksEndpoint: kStacksEndpoint,
});
if (!(last_executed_tx_nonce < nonce)) {
console.log(
`Invalid sponsor nonce: ${nonce}, last executed: ${last_executed_tx_nonce}`,
);
return;
}
const pgPool = await getPgPool();
const replacer = await pgPool.maybeOne(sql.type(UserOperation)`
SELECT * FROM "user_operations" WHERE "tx_id" = ${sql.binary(tx_id)}`);
if (replacer == null) {
console.log(`User operation 0x${tx} not found`);
return;
}
const { last_executed_tx_nonce: user_last_executed_nonce } =
await getAccountNonces(replacer.sender, {
stacksEndpoint: kStacksEndpoint,
});
if (replacer.nonce <= BigInt(user_last_executed_nonce)) {
console.log(
`Invalid user operation 0x${tx} nonce ${replacer.nonce}, last executed: ${user_last_executed_nonce}`,
);
return;
}
await pgPool.transaction(async client => {
const currentOperation = await client.maybeOne(sql.type(UserOperation)`
SELECT * FROM "user_operations"
WHERE "sponsor" = ${sponsorAccount}
AND "sponsor_nonce" = ${nonce}
AND "status" = 'submitted'`);
if (currentOperation != null && !currentOperation.tx_id.equals(tx_id)) {
await client.query(sql.typeAlias('void')`
UPDATE "sponsor_records"
SET "status" = 'failed',
"error" = 'dropped by another operation'
WHERE "tx_id" = ${sql.binary(currentOperation.tx_id)}`);
await client.query(sql.typeAlias('void')`
UPDATE "user_operations"
SET "status" = 'failed',
"error" = 'dropped by another operation'
WHERE "id" = ${currentOperation.id}`);
}
await client.query(sql.typeAlias('void')`
UPDATE "sponsor_records"
SET "status" = 'failed',
"error" = 'dropped and processed with another nonce'
WHERE "tx_id" = ${sql.binary(replacer.tx_id)}`);
await client.query(sql.typeAlias('void')`
UPDATE "user_operations"
SET "sponsor" = ${sponsorAccount},
"sponsor_nonce" = ${nonce}
WHERE "id" = ${replacer.id}`);
});
const user_tx = deserializeTransaction(replacer.raw_tx);
const user_tx_id = user_tx.txid();
const network =
kStacksNetworkType === 'mocknet'
? new StacksMocknet({ url: kStacksEndpoint })
: new StacksMainnet({ url: kStacksEndpoint });
const nodeInfo = await getNodeInfo({ stacksEndpoint: kStacksEndpoint });
const sponsored_tx = await sponsorTransaction({
transaction: user_tx,
sponsorPrivateKey: account.secretKey,
network,
fee: gas,
sponsorNonce: nonce,
});
// record first and then submit
await pgPool.query(sql.typeAlias(
'void',
)`INSERT INTO "public"."sponsor_records"
(tx_id, raw_tx, sender, nonce, contract_address, function_name, args, fee, sponsor, sponsor_tx_id, sponsor_nonce, submit_block_height, status, created_at, updated_at) VALUES
(
${sql.binary(replacer.tx_id)},
${sql.binary(replacer.raw_tx)},
${replacer.sender}, ${String(replacer.nonce)},
${replacer.contract_address},
${replacer.function_name},
${JSON.stringify(replacer.args)},
${String(gas)},
${account.address},
${sql.binary(Buffer.from(sponsored_tx.txid(), 'hex'))},
${String(replacer.sponsor_nonce)},
${nodeInfo.stacks_tip_height},
'pending',
NOW(),
NOW()
)`);
const rs = await broadcastTransaction(sponsored_tx, network);
if (rs.reason == null) {
console.log(
`Submitted user tx 0x${user_tx_id} by 0x${sponsored_tx.txid()}`,
);
await pgPool.query(sql.typeAlias('void')`
UPDATE user_operations
SET sponsor_tx_id = ${sql.binary(Buffer.from(sponsored_tx.txid(), 'hex'))},
submit_block_height = ${nodeInfo.stacks_tip_height},
fee = ${gas},
updated_at = NOW()
WHERE id = ${replacer.id}`);
await pgPool.query(sql.typeAlias('void')`
UPDATE sponsor_records
SET status = 'submitted',
fee = ${gas},
updated_at = NOW()
WHERE tx_id = ${sql.binary(replacer.tx_id)}
AND sponsor_tx_id = ${sql.binary(Buffer.from(sponsored_tx.txid(), 'hex'))}`);
} else {
console.error(
`Fail to broadcast tx ${rs.txid}, error: ${rs.error}, reason: ${
rs.reason
}, reason_data: ${stringify(rs, null, 2)}`,
);
await pgPool.query(sql.typeAlias('void')`
UPDATE user_operations
SET sponsor_tx_id = ${sql.binary(Buffer.from(sponsored_tx.txid(), 'hex'))},
fee = ${gas},
status = 'failed',
error = ${rs.reason ?? 'N/A'},
updated_at = NOW()
WHERE id = ${replacer.id}`);
await pgPool.query(sql.typeAlias('void')`
DELETE FROM "sponsor_records"
WHERE tx_id = ${sql.binary(replacer.tx_id)}
AND sponsor_tx_id = ${sql.binary(Buffer.from(sponsored_tx.txid(), 'hex'))}`);
}
}
main().catch(console.error);

View File

@@ -1,5 +1,5 @@
import { sql } from 'slonik';
import { kStacksNetworkType } from 'src/config';
import { kStacksNetworkType } from '../config';
import { getPgPool } from '../db';
async function main() {

View File

@@ -1,6 +1,5 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": ["es2020"],
"target": "es2020",
"strict": true,

View File

@@ -521,6 +521,11 @@
"@types/mime" "*"
"@types/node" "*"
"@types/yargs-parser@^21.0.3":
version "21.0.3"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==
"@typescript-eslint/eslint-plugin@^7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz#407daffe09d964d57aceaf3ac51846359fbe61b0"
@@ -3517,6 +3522,11 @@ yaml@2.3.4:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"