I've been trying to reuse a wallet deleted by DESTROY_ACCOUNT_IF_ZERO (32) flag. The way I do the transfer:
Imagine we have 2 wallets: A and B.
- We transfer
N1TON fromAtoB. E.g. by using a web-based wallet, script, doesn't matter. Bis uninitialized - it is not on the blockchain yet. With that in mind, we'd like to send all the money fromBback toA. For this we're going to use the attached script.- Now we go ahead and transfer
N2TON fromAtoBagain. Doesn't matter how much.
Expected behavior: we get transactions: A->B (N1), B->A (all), A->B (N2); wallet B has N2 TONs in the end.
Actual behavior: we get transactions A->B (N1), B->A (all), A->B (N2), B->A (all); wallet B has 0 TONs in the end.
The script for B->A (all) transfer, which causes this behavior:
import TonWeb from 'tonweb';
import tonwebMnemonic from 'tonweb-mnemonic';
const apiKey = "" || undefined;
// WALLET B
const mnemonic = "...";
const walletVersion = "v4R2";
// WALLET A
const toAddress = "...";
const SendMode = {
CARRY_ALL_REMAINING_BALANCE: 128,
CARRY_ALL_REMAINING_INCOMING_VALUE: 64,
DESTROY_ACCOUNT_IF_ZERO: 32,
PAY_GAS_SEPARATELY: 1,
IGNORE_ERRORS: 2,
NONE: 0
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
(async () => {
const provider = new TonWeb.HttpProvider('https://toncenter.com/api/v2/jsonRPC', {apiKey});
const WalletClass = TonWeb.Wallets.all[walletVersion];
const mnemonicArray = mnemonic.split(" ");
let { publicKey, secretKey } = await tonwebMnemonic.mnemonicToKeyPair(mnemonicArray);
publicKey = Buffer.from(publicKey);
secretKey = Buffer.from(secretKey);
console.log(`PUBLIC KEY: ${publicKey.toString('hex')}`);
const wallet = new WalletClass(provider, { publicKey });
const seqno = await wallet.methods.seqno().call() || 0;
console.log(`SEQNO: ${seqno}`);
await sleep(2000);
const transferParams = {
secretKey,
toAddress,
amount: 0,
seqno,
sendMode: SendMode.CARRY_ALL_REMAINING_BALANCE | SendMode.IGNORE_ERRORS | SendMode.DESTROY_ACCOUNT_IF_ZERO,
payload: "test bug"
};
console.log(JSON.stringify({...transferParams, secretKey: transferParams.secretKey.toString('hex')}, null, 2));
const response = await wallet.methods.transfer(transferParams).send();
console.log(`transfer sent to blockchain: ${JSON.stringify(response, null, 2)}`);
})();
The strange thing is: if you send A->B (N) again, it will create a new transfer B->A (all) again and again automatically. Sometimes it stops at 2 repeats, sometimes at 4.
I use the unbounceable address: UQAPqRlewultl8xHCKGsrenb4PZaQ0QDfPYoK1fwVUODdZRd.
I also tried sending TON from wallet C (C->B). And the money DID stay on B. BUT once I made a transfer A->B again, all the money including the TONs sent from C went to A automatically!
An example of this bug's manifestation (is it a bug??) is here:
https://tonviewer.com/EQAySjlsHUY2EEedO5GTenzFvAnR5E-4ptwfeox6OZYnNvGF
This is B wallet. Only the first message with "test bug" text was sent by the script. The rest of them were sent automatically on any incoming transfer from A.
In this interaction:
AisUQAPqRlewultl8xHCKGsrenb4PZaQ0QDfPYoK1fwVUODdZRdBisEQAySjlsHUY2EEedO5GTenzFvAnR5E-4ptwfeox6OZYnNvGFCisUQBwpXsIVrij8UQ2OpPK2EyeVBrlN6mnOkmThb3k6K-UHZyL
I want to understand, why this happens. Is this intended by TON blockchain developers? Is this not a bug? How to go around this and reuse deleted wallets?
It's not a bug.
Incoming messages to the blockchain are kept for a while in node's mempool, until their TTL runs out.
Initial state of our wallet's smart contract is
seqno=0.Flag
DESTROY_ACCOUNT_IF_ZERO(32) destroys account, so wallet's smart contract no longer exists in the blockchain. We make this transfer withseqno=0.After the outgoing transfer is sent from the wallet, wallet's smart contract gets published to the blockchain again. Which in turn means its
seqnobecomes0again. So the previous messages, still in the mempool, get executed again.So, it is, in fact, a really peculiar behavior of TON blockchain. It is very irregular, indeed, but in no way is it a bug.