Skip to content

Instantly share code, notes, and snippets.

@teidesu
Last active October 5, 2025 01:01
Show Gist options
  • Select an option

  • Save teidesu/1424bd8055e5a4bb021a055d84d81301 to your computer and use it in GitHub Desktop.

Select an option

Save teidesu/1424bd8055e5a4bb021a055d84d81301 to your computer and use it in GitHub Desktop.
script to recover funds (ton and usdt) from a ledger-managed toncoin wallet. test with a small amount first!!!
/* eslint-disable no-console */
import { getHttpEndpoint } from '@orbs-network/ton-access'
import { keyPairFromSeed } from '@ton/crypto'
import { Address, beginCell, external, internal, storeMessage, toNano, TonClient, WalletContractV4 } from '@ton/ton'
import { mnemonicToSeed as mnemonicToSeedBip39 } from 'bip39'
import { deriveED25519Path } from './ed25519.ts' // https://github.com/tonkeeper/tonkeeper-web/blob/0b47a3d6523065d53d2d32c4a82a644bad797fb0/packages/core/src/service/ed25519.ts
// for whatever reason ledger uses a non-standard bip39 path lol.
// and since there aren't any gui wallets (i know of) that support custom derivation paths, i had to resort to this
// thanks https://www.reddit.com/r/ledgerwallet/comments/1g8kri0/comment/lt4vf9f/
// p.s. you might need to tweak the path further for sub-wallets
const mnemonic = 'pumpkin ...'
const bip39Path = "m/44'/607'/0'/0'/0'/0'"
const toAddress = Address.parse('UQBt....')
const amount = 1000n * 1000000n // 1000 USDT
// how much ton to use for the transaction. some of it will be used as a fee, the rest is sent to the recipient
const tonAmount = '0.5'
const endpoint = await getHttpEndpoint({ network: 'mainnet' })
const ton = new TonClient({ endpoint })
const seed = await mnemonicToSeedBip39(mnemonic)
const seedContainer = deriveED25519Path(bip39Path, seed.toString('hex'))
const keyPair = keyPairFromSeed(seedContainer.key)
const wallet = WalletContractV4.create({
publicKey: keyPair.publicKey,
workchain: 0,
})
async function getUserJettonWalletAddress(userAddress: Address, jettonMasterAddress: Address) {
const userAddressCell = beginCell().storeAddress(userAddress).endCell()
const response = await ton.runMethod(jettonMasterAddress, 'get_wallet_address', [
{ type: 'slice', cell: userAddressCell },
])
return response.stack.readAddress()
}
const myAddress = wallet.address.toString({ bounceable: false })
console.log(myAddress)
console.log('verify the address matches before proceeding!')
process.exit(1)
// thanks https://github.com/ton-org/ton/issues/44#issuecomment-2193904906
const contract = ton.open(wallet)
const balance = await contract.getBalance()
const seqno = await contract.getSeqno()
console.log(balance, seqno)
const jettonWalletAddress = await getUserJettonWalletAddress(Address.parse(myAddress), Address.parse('EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs')) // usdt contract
console.log(jettonWalletAddress)
const messageBody = beginCell()
.storeUint(0x0F8A7EA5, 32) // opcode for jetton transfer
.storeUint(0, 64) // query id
.storeCoins(amount)
.storeAddress(toAddress)
.storeAddress(toAddress) // response destination
.storeBit(0) // no custom payload
.storeCoins(0) // forward amount - if > 0, will send notification message
.storeBit(0) // we store forwardPayload as a reference, set 1 and uncomment next line for have a comment
// .storeRef(forwardPayload)
.endCell()
const internalMessage = internal({
to: jettonWalletAddress,
value: toNano(tonAmount),
bounce: true,
body: messageBody,
})
const body = wallet.createTransfer({
seqno,
secretKey: keyPair.secretKey,
messages: [internalMessage],
})
const externalMessage = external({
to: myAddress,
body,
})
const externalMessageCell = beginCell().store(storeMessage(externalMessage)).endCell()
const signedTransaction = externalMessageCell.toBoc()
const hash = externalMessageCell.hash().toString('hex')
console.log('tx hash:', hash)
await ton.sendFile(signedTransaction)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment