[Example] Replacing Pending TXs

Tx-Tracker

Tx Tracker์˜ ์‚ฌ์šฉ๋ฒ• ๋ฐ ํ™œ์šฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ๋ถ„์€ ์ด ํŠœํ† ๋ฆฌ์–ผ์„ ํ†ตํ•ด

  • Tx-Tracker์˜ ์‚ฌ์šฉ๋ฒ•์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Tx-Tracker๋ฅผ ์‚ฌ์šฉํ•ด์„œ pending ์ƒํƒœ์˜ transaction์„ auto-resolvingํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, ํŠœํ† ๋ฆฌ์–ผ์˜ ์›ํ™œํ•œ ์ง„ํ–‰์„ ์œ„ํ•ด ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ๋ถ„์€ ์ด ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ต

  • ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜์—ฌ ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ์— ์ „ํŒŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ฐœ์ƒ์‹œํ‚จ ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ๋ฅผ ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠœํ† ๋ฆฌ์–ผ์—์„œ๋Š” pending ์ƒํƒœ์˜ transaction์ด auto-resolve๊ฐ€ ๋˜๋Š” ๊ณผ์ •์„ ๋ณด์ด๊ธฐ ์œ„ํ•ด Tx-Tracker์—์„œ๋Š” ์ œ๊ณตํ•˜์ง€ ์•Š๋Š” replaced๋ผ๋Š” status๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. transaction์ด pending์ด ๋˜๋Š” ๊ฒฝ์šฐ๋Š” ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋•Œ, ์˜ค๋žซ๋™์•ˆ pending์ด ๋˜๋Š” ์ƒํƒœ๋ฅผ ๋ง‰๊ณ ์ž pending์ƒํƒœ์ธ transaction์˜ nonce์™€ ๊ฐ™์€ nonce, ๋” ๋†’์€ gas price๋กœ ์ƒˆ๋กœ์šด transaction์„ ์ƒ์„ฑํ•˜๋ฉฐ ์ฑ„๊ตด ๊ฒฝ์Ÿ์—์„œ ๋ฐ€๋ฆฐ transaction์˜ ์ƒํƒœ replaced๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

Project Structure

./sample-tx-tracker
sample-tx-tracker/
โ”œโ”€โ”€ /config
โ”œโ”€โ”€ /helper # helper library for make transaction
โ”œโ”€โ”€ /public
โ”œโ”€โ”€ /src # frontend code
โ”œโ”€โ”€ /types # types used in index.js
โ”œโ”€โ”€ /store # transaction store used in index.js
โ”œโ”€โ”€ .env # configuration file
โ”œโ”€โ”€ index.js # API server code
โ”œโ”€โ”€ jsconfig.json
โ”œโ”€โ”€ package.json
...
  • API ์„œ๋ฒ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

    • POST /api/tx: ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜๊ณ  ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ์— ์ „ํŒŒํ•ฉ๋‹ˆ๋‹ค.

    • GET /api/tx : ๋„คํฌ์›Œํฌ์— ์ „ํŒŒํ•œ ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  • Frontend

    • API ์„œ๋ฒ„์˜ GET /api/tx ๋ฅผ ์ง€์†์ ์œผ๋กœ ํด๋งํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.

    • ์œ ์ €๊ฐ€ Generate Transaction ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด, POST /api/tx ์š”์ฒญ์„ API์„œ๋ฒ„์— ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

Step By Step

  1. Sample repository ๊ฐ€์ ธ์˜ค๊ธฐ

    git clone https://github.com/HAECHI-LABS/sample-tx-tracker
  2. ์˜์กด์„ฑ ์„ค์น˜ํ•˜๊ธฐ

    npm install
  3. .env ๋ณ€๊ฒฝํ•˜๊ธฐ

    • CLIENT_ID : henesis account:describe ๋ฅผ ํ†ตํ•ด ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • PRIVATE_KEY : ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ง€๊ฐ‘์˜ ํ”„๋ผ์ด๋น— ํ‚ค์ž…๋‹ˆ๋‹ค. ์ถฉ๋ถ„ํ•œ ์–‘์˜ ์ด๋”๋ฆฌ์›€์ด ์žˆ๋Š” EOA ์ฃผ์†Œ์˜ ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.

    • NODE_ENDPOINT : ์—ฐ๊ฒฐํ•  ๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ endpoint ์ž…๋‹ˆ๋‹ค.

    • PLATFORM: ์›ํ•˜๋Š” ํ”Œ๋žซํผ์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ์ง€์› ๊ฐ€๋Šฅํ•œ ํ”Œ๋žซํผ ๋ฐ ๋„คํŠธ์›Œํฌ๋Š” ์—ฌ๊ธฐ์—์„œ ํ™•์ธ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    • NETWORK: ์›ํ•˜๋Š” ๋„คํŠธ์›Œํฌ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

    CLIENT_ID=<your-client-id>
    PRIVATE_KEY=<your private key>
    NODE_ENDPOINT=https://ropsten.infura.io/v3/<your-key>
    PLATFORM=ethereum
    NETWORK=ropsten
  4. ์†Œ์Šค์ฝ”๋“œ๋ฅผ ๋นŒ๋“œํ•˜๊ธฐ

    npm run build:standalone
  5. ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๋ธŒ๋ผ์šฐ์ €์— http://localhost:3000 ํƒ์ƒ‰ํ•˜๊ธฐ

    node index.js

์ž‘๋™์›๋ฆฌ

Transaction tracker using Henesis SDK

index.js ๊ฐ€ ์ด๋ฒˆ ํŠœํ† ๋ฆฌ์–ผ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ด๊ณณ์—์„œ Henesis SDK๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

CLIENT_ID ๋ฅผ ์ด์šฉํ•˜์—ฌ Henesis Server์™€์˜ ์ธ์ฆ์„ ์ง„ํ–‰ํ•˜๊ณ  ์›ํ•˜๋Š” platform๊ณผ network๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๊ตฌ๋…์„ ์œ„ํ•œ Henesis ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์ฐธ๊ณ ).

index.js
const {CLIENT_ID, PRIVATE_KEY, NODE_ENDPOINT, PLATFORM, NETWORK} = process.env;
const tracker = new TransactionTracker(CLIENT_ID, {
platform: PLATFORM,
network: NETWORK
});

API ์„œ๋ฒ„๊ฐ€ henesis#trackTransaction ๋ฅผ ์ด์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์„ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค(์ฐธ๊ณ ).

  • ํŠธ๋žœ์žญ์…˜์„ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  transactionHash ๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค.

  • transactionHash ๋ฅผ henesis#trackTransaction ์— ๋„ฃ์–ด ์ถ”์ ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  • timeout๊ณผconfirmation์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

index.js
app.post('/api/tx', async function (req, res) {
//Generate Transactions
const nonce = await sender.getNonce();
const transactionHash = await sender.send(nonce, GAS_PRICE);
console.log(`transaction generated. txHash:${transactionHash}`);
โ€‹
//start tracking transaction
await tracker.trackTransaction(transactionHash, {
timeout: TIMEOUT,
confirmation: CONFIRMATION
});
โ€‹
const transaction = new Transaction(
transactionHash,
nonce,
GAS_PRICE
);
transactionStore.save(transaction);
await res.json(transaction);
});

subscription ๊ตฌ๋…์„ ํ†ตํ•ด trackingํ•˜๊ณ  ์žˆ๋Š” transaction์˜ ์ƒํƒœ ๋ฐ ์ •๋ณด๋ฅผ ๋ฐ›์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • message.data.type ์—์„œ ์ถ”์ ํ•œ ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • message.data.result ์—์„œ ์ถ”์ ํ•œ ํŠธ๋žœ์žญ์…˜์˜ ์ •๋ณด๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • message.ack() ๋ฅผ ๋งˆ์ง€๋ง‰์— ๋ฐ˜๋“œ์‹œ ํ˜ธ์ถœํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Tx-Tracker์—์„œ trackingํ•˜๋Š” status๋Š” pending, receipt, confirmation ์„ธ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ pending ์ƒํƒœ๋ผ๋ฉด transaction์„ resolveํ•˜๊ธฐ ์œ„ํ•œ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ „ transaction๋“ค์˜ nonce๋ฅผ ํ™•์ธํ•˜์—ฌ resolve๊ฐ€ ํ•„์š”ํ•œ transaction์ผ ๊ฒฝ์šฐ์— resolve๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ Tx-Tracker๋Š” pending ์ƒํƒœ์˜ transaction์ผ ๋•Œ, transaction data๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— auto-resolving์„ ์œ„ํ•ด TransactionStore๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

transaction์ด ์ฑ„๊ตด๋˜๋ฉด, receipt ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. checkResolvedTransaction ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋งŒ์•ฝ resolve๊ฐ€ ๋œ transaction์ด๋ผ๋ฉด ์ฑ„๊ตด๊ฒฝ์Ÿ์—์„œ ๋ฐ€๋ฆฐ transaction์˜ ์ƒํƒœ๋ฅผ replaced ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.

index.js
async function trackTx() {
const subscription = await tracker.subscribe(
"transaction",
{
subscriptionId: "your-subscription-id",
ackTimeout: 30 * 1000 // default is 10 * 1000 (ms)
}
);
โ€‹
subscription.on("message", async (message) => {
const transactionHash = message.data.result.transactionHash;
let transaction = {};
console.log(`[MESSAGE] transaction ${transactionHash} status is: ${message.data.type}`)
switch (message.data.type) {
case 'pending' :
transaction = transactionStore.findByHash(transactionHash);
if (transaction.status == undefined) {
transaction.status = Status.pending;
}
if (isNeededResolve(transaction)) {
const newTransaction = await retry(transaction);
transactionStore.save(newTransaction);
}
break;
case 'receipt' :
transaction = transactionStore.findByHash(transactionHash);
checkResolvedTransaction(transaction);
transaction.status = Status.receipt;
transaction.data = {...message.data.result};
transactionStore.save(transaction);
break;
case 'confirmation' :
transaction = transactionStore.findByHash(transactionHash);
transaction.status = Status.confirmation;
transaction.data = {...message.data.result};
transactionStore.save(transaction);
break;
}
message.ack();
});
โ€‹
subscription.on("error", async (error) => {
console.log('err', error);
});
}