Skip to content

Commit 1d8a767

Browse files
authored
feat(cli): add command for debugging gateway transactions (#39)
1 parent f706753 commit 1d8a767

File tree

3 files changed

+252
-1
lines changed

3 files changed

+252
-1
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ test: ## Run contract tests
1313
deploy: ## Run deployment script
1414
npx blueprint run deploy
1515

16+
debug-tx: ## Execute a transaction to the Gateway
17+
@npx blueprint run debugTransaction
18+
1619
tx: ## Execute a transaction to the Gateway
1720
npx blueprint run transaction
1821

@@ -33,4 +36,4 @@ lint: ## Lint the code
3336
fmt: ## Format the code
3437
npm run prettier-fix
3538

36-
.PHONY: help compile test deploy tx tx-localnet debug fmt
39+
.PHONY: help compile test deploy tx tx-localnet debug debug-tx fmt

scripts/common/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,50 @@ export async function inputGateway(provider: NetworkProvider): Promise<Address>
1313
.ui()
1414
.inputAddress('Enter Gateway address', isTestnet ? GATEWAY_ACCOUNT_ID_TESTNET : undefined);
1515
}
16+
17+
export async function inputNumber(
18+
provider: NetworkProvider,
19+
prompt: string,
20+
defaultValue: number,
21+
min = 1,
22+
max = 100,
23+
): Promise<number> {
24+
const input = await provider.ui().input(`${prompt} (default is ${defaultValue})`);
25+
if (input === '') {
26+
return defaultValue;
27+
}
28+
29+
const number = parseInt(input);
30+
31+
if (isNaN(number) || number < min || number > max) {
32+
console.log(`Invalid number, using default value ${defaultValue}`);
33+
return defaultValue;
34+
}
35+
36+
return number;
37+
}
38+
39+
export function parseTxHash(txHash: string): { lt: string; hash: string } {
40+
const chunks = txHash.split(':');
41+
if (chunks.length !== 2) {
42+
throw new Error(`Invalid transaction hash "${txHash}"`);
43+
}
44+
45+
const lt = chunks[0];
46+
47+
// input requires hex, but ton client accepts base64
48+
const hash = Buffer.from(chunks[1], 'hex').toString('base64');
49+
50+
return { lt, hash };
51+
}
52+
53+
export function addressLink(address: Address, isTestnet: boolean): string {
54+
const raw = address.toRawString();
55+
return isTestnet
56+
? `https://testnet.tonscan.org/address/${raw}`
57+
: `https://tonscan.org/address/${raw}`;
58+
}
59+
60+
export function txLink(hash: string, isTestnet: boolean): string {
61+
return isTestnet ? `https://testnet.tonscan.org/tx/${hash}` : `https://tonscan.org/tx/${hash}`;
62+
}

scripts/debugTransaction.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { NetworkProvider } from '@ton/blueprint';
2+
import { Gateway } from '../wrappers/Gateway';
3+
import * as common from './common';
4+
import {
5+
CommonMessageInfoExternalIn,
6+
CommonMessageInfoInternal,
7+
fromNano,
8+
OpenedContract,
9+
Transaction,
10+
} from '@ton/core';
11+
import { TonClient } from '@ton/ton';
12+
import { bufferToHexString, depositLogFromCell, GatewayOp, sliceToHexString } from '../types';
13+
14+
let isTestnet = false;
15+
16+
export async function run(provider: NetworkProvider) {
17+
isTestnet = provider.network() === 'testnet';
18+
19+
const client = resolveClient(provider);
20+
21+
const gwAddress = await common.inputGateway(provider);
22+
const gw = await provider.open(Gateway.createFromAddress(gwAddress));
23+
24+
const commands: Record<string, string> = {
25+
'recent-txs': 'List recent transactions',
26+
'specific-tx': 'Explore specific transaction',
27+
};
28+
29+
const cmd = await provider
30+
.ui()
31+
.choose('Select command', Object.keys(commands), (cmd) => commands[cmd]);
32+
33+
if (cmd === 'recent-txs') {
34+
const limit = await common.inputNumber(provider, 'Enter tx limit', 20);
35+
36+
await suppressException(async () => await fetchLastTransactions(client, gw, limit));
37+
return;
38+
}
39+
40+
await suppressException(async () => {
41+
const txHash = await provider.ui().input(`Enter transaction in a format <lt>:<hash>`);
42+
await fetchTransaction(client, gw, txHash);
43+
});
44+
}
45+
46+
async function fetchLastTransactions(client: TonClient, gw: OpenedContract<Gateway>, limit = 10) {
47+
const txs = await client.getTransactions(gw.address, { limit, archival: true });
48+
49+
for (const tx of txs) {
50+
const parsed = parseTransaction(tx);
51+
console.log(parsed);
52+
}
53+
}
54+
55+
async function fetchTransaction(client: TonClient, gw: OpenedContract<Gateway>, txHash: string) {
56+
const { lt, hash } = common.parseTxHash(txHash);
57+
58+
let tx: Transaction | undefined;
59+
60+
try {
61+
const txs = await client.getTransactions(gw.address, {
62+
limit: 1,
63+
lt,
64+
hash,
65+
inclusive: true,
66+
archival: true,
67+
});
68+
if (txs.length === 0) {
69+
console.error(`Transaction "${txHash}" not found`);
70+
return;
71+
}
72+
73+
tx = txs[0];
74+
} catch (error) {
75+
console.error('getTransactions', error);
76+
return;
77+
}
78+
79+
const parsed = parseTransaction(tx);
80+
81+
console.log('Transaction details', parsed);
82+
}
83+
84+
function resolveClient(provider: NetworkProvider): TonClient {
85+
const api = provider.api();
86+
87+
if (api instanceof TonClient) {
88+
return api;
89+
}
90+
91+
throw new Error('API is not a TonClient instance');
92+
}
93+
94+
async function suppressException(fn: () => Promise<void>) {
95+
try {
96+
await fn();
97+
} catch (error) {
98+
console.error(error instanceof Error ? error.message : error);
99+
}
100+
}
101+
102+
function parseTransaction(tx: Transaction) {
103+
return tx.inMessage?.info.type === 'internal' ? parseInbound(tx) : parseOutbound(tx);
104+
}
105+
106+
function parseInbound(tx: Transaction) {
107+
const info = tx.inMessage!.info as CommonMessageInfoInternal;
108+
const hash = tx.hash().toString('hex');
109+
110+
let kv: Record<string, any> = {};
111+
112+
const slice = tx.inMessage!.body.beginParse();
113+
const opCode = slice.loadUint(32);
114+
115+
switch (opCode) {
116+
case GatewayOp.Donate:
117+
kv.operation = 'donate';
118+
kv.queryId = slice.loadUint(64);
119+
120+
break;
121+
case GatewayOp.Deposit:
122+
kv.operation = 'deposit';
123+
kv.queryId = slice.loadUint(64);
124+
kv.zevmRecipient = bufferToHexString(slice.loadBuffer(20));
125+
126+
const outDeposit = depositLogFromCell(tx.outMessages.get(0)!.body);
127+
kv.depositAmount = formatCoin(outDeposit.amount);
128+
kv.depositFee = formatCoin(outDeposit.depositFee);
129+
130+
break;
131+
case GatewayOp.DepositAndCall:
132+
kv.operation = 'deposit_and_call';
133+
kv.queryId = slice.loadUint(64);
134+
kv.zevmRecipient = bufferToHexString(slice.loadBuffer(20));
135+
kv.callData = sliceToHexString(slice.loadRef().asSlice());
136+
137+
const outDepositAndCall = depositLogFromCell(tx.outMessages.get(0)!.body);
138+
kv.depositAmount = formatCoin(outDepositAndCall.amount);
139+
kv.depositFee = formatCoin(outDepositAndCall.depositFee);
140+
141+
break;
142+
default:
143+
kv.operation = `unknown (op: ${opCode})`;
144+
}
145+
146+
return {
147+
sender: info.src.toRawString(),
148+
receiver: info.dest.toRawString(),
149+
hash: `${tx.lt}:${hash}`,
150+
timestamp: formatDate(tx.now),
151+
txAmount: formatCoin(info.value.coins),
152+
gas: formatCoin(tx.totalFees.coins),
153+
link: common.txLink(hash, isTestnet),
154+
payload: kv,
155+
};
156+
}
157+
158+
function parseOutbound(tx: Transaction) {
159+
const info = tx.inMessage!.info as CommonMessageInfoExternalIn;
160+
const hash = tx.hash().toString('hex');
161+
162+
const slice = tx.inMessage!.body.beginParse();
163+
164+
// [V, R, S]
165+
const signature = slice.loadBuffer(1 + 32 + 32);
166+
167+
const payload = slice.loadRef().beginParse();
168+
169+
const opCode = payload.loadUint(32);
170+
if (opCode !== GatewayOp.Withdraw) {
171+
throw new Error(`Unsupported outbound op code: ${opCode}`);
172+
}
173+
174+
const recipient = payload.loadAddress();
175+
const amount = payload.loadCoins();
176+
const seqno = payload.loadUint(32);
177+
178+
return {
179+
sender: null, // external messages don't have a sender
180+
receiver: info.dest.toRawString(),
181+
hash: `${tx.lt}:${hash}`,
182+
timestamp: formatDate(tx.now),
183+
gas: formatCoin(tx.totalFees.coins),
184+
link: common.txLink(hash, isTestnet),
185+
payload: {
186+
operation: 'withdraw',
187+
signature: `0x${signature.toString('hex')}`,
188+
recipient: recipient.toRawString(),
189+
amount: formatCoin(amount),
190+
seqno,
191+
},
192+
};
193+
}
194+
195+
function formatDate(at: number) {
196+
return new Date(at * 1000).toISOString();
197+
}
198+
199+
function formatCoin(amount: bigint) {
200+
return `${fromNano(amount)} TON`;
201+
}

0 commit comments

Comments
 (0)