mirror of
https://github.com/alexgo-io/stacks-transaction-sponsor.git
synced 2026-01-12 22:24:18 +08:00
feat: sponsor tx replacer cli
Signed-off-by: bestmike007 <i@bestmike007.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
173
src/cli/replace-sponsor-nonce.ts
Normal file
173
src/cli/replace-sponsor-nonce.ts
Normal 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);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sql } from 'slonik';
|
||||
import { kStacksNetworkType } from 'src/config';
|
||||
import { kStacksNetworkType } from '../config';
|
||||
import { getPgPool } from '../db';
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"lib": ["es2020"],
|
||||
"target": "es2020",
|
||||
"strict": true,
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user