tx-broadcaster.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { StacksTestnet } from '@stacks/network';
  2. import { StackingClient } from '@stacks/stacking';
  3. import {
  4. TransactionVersion,
  5. getAddressFromPrivateKey,
  6. getNonce,
  7. makeSTXTokenTransfer,
  8. broadcastTransaction,
  9. StacksTransaction,
  10. } from '@stacks/transactions';
  11. import { writeFileSync } from 'fs';
  12. import { logger } from './common';
  13. const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2');
  14. const url = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`;
  15. const network = new StacksTestnet({ url });
  16. const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0');
  17. const PAUSE_HEIGHT = parseInt(process.env.PAUSE_HEIGHT ?? '999999999999');
  18. const PAUSE_TIMER = parseInt(process.env.PAUSE_TIMER ?? '86400000');
  19. const accounts = process.env.ACCOUNT_KEYS!.split(',').map(privKey => ({
  20. privKey,
  21. stxAddress: getAddressFromPrivateKey(privKey, TransactionVersion.Testnet),
  22. }));
  23. const client = new StackingClient(accounts[0].stxAddress, network);
  24. async function run() {
  25. const poxInfo = await client.getPoxInfo();
  26. if (poxInfo.current_burnchain_block_height == PAUSE_HEIGHT) {
  27. logger.info(
  28. `Pause height reached : (current=${poxInfo.current_burnchain_block_height}), (pause=${PAUSE_HEIGHT})`
  29. );
  30. logger.info(
  31. `sleeping for ${PAUSE_TIMER}`
  32. )
  33. await new Promise(resolve => setTimeout(resolve, PAUSE_TIMER));
  34. }
  35. const accountNonces = await Promise.all(
  36. accounts.map(async account => {
  37. const nonce = await getNonce(account.stxAddress, network);
  38. return { ...account, nonce };
  39. })
  40. );
  41. // Send from account with lowest nonce
  42. accountNonces.sort((a, b) => Number(a.nonce) - Number(b.nonce));
  43. const sender = accountNonces[0];
  44. const recipient = accountNonces[1];
  45. logger.info(
  46. `Sending stx-transfer from ${sender.stxAddress} (nonce=${sender.nonce}) to ${recipient.stxAddress}`
  47. );
  48. const tx = await makeSTXTokenTransfer({
  49. recipient: recipient.stxAddress,
  50. amount: 1000,
  51. senderKey: sender.privKey,
  52. network,
  53. nonce: sender.nonce,
  54. fee: 300,
  55. anchorMode: 'any',
  56. });
  57. await broadcast(tx, sender.stxAddress);
  58. }
  59. async function broadcast(tx: StacksTransaction, sender?: string) {
  60. const txType = tx.payload.payloadType;
  61. const label = sender ? accountLabel(sender) : 'Unknown';
  62. const broadcastResult = await broadcastTransaction(tx, network);
  63. if (broadcastResult.error) {
  64. logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`);
  65. return false;
  66. } else {
  67. if (label.includes('Flooder')) return true;
  68. logger.debug(`Broadcast ${txType} from ${label} tx=${broadcastResult.txid}`);
  69. return true;
  70. }
  71. }
  72. async function waitForNakamoto() {
  73. while (true) {
  74. try {
  75. const poxInfo = await client.getPoxInfo();
  76. if (poxInfo.current_burnchain_block_height! <= EPOCH_30_START) {
  77. logger.info(
  78. `Nakamoto not activated yet, waiting... (current=${poxInfo.current_burnchain_block_height}), (epoch3=${EPOCH_30_START})`
  79. );
  80. } else {
  81. logger.info(
  82. `Nakamoto activation height reached, ready to submit txs for Nakamoto block production`
  83. );
  84. break;
  85. }
  86. } catch (error) {
  87. if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) {
  88. logger.info(
  89. `Stacks node not ready, waiting...`
  90. );
  91. } else {
  92. logger.error('Error getting pox info:', error);
  93. }
  94. }
  95. await new Promise(resolve => setTimeout(resolve, 3000));
  96. }
  97. }
  98. function accountLabel(address: string) {
  99. const accountIndex = accounts.findIndex(account => account.stxAddress === address);
  100. if (accountIndex !== -1) {
  101. return `Account #${accountIndex}`;
  102. }
  103. return `Unknown (${address})`;
  104. }
  105. async function loop() {
  106. await waitForNakamoto();
  107. // Signal readiness to Kubernetes
  108. writeFileSync('/tmp/ready', 'true');
  109. logger.info('Nakamoto activated, broadcaster is ready');
  110. while (true) {
  111. try {
  112. await run();
  113. } catch (e) {
  114. console.log(e);
  115. logger.error('Error submitting stx-transfer tx:', e);
  116. }
  117. await new Promise(resolve => setTimeout(resolve, broadcastInterval * 1000));
  118. }
  119. }
  120. loop();