From 775294e5c18e69ca8873bd953c20f107fd20c156 Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Thu, 6 Mar 2025 19:09:22 +0200 Subject: [PATCH] fix(@fireblocks/recovery-utility): :bug: fix issue: jettons withdrawals with wallet seqno 0 --- .../lib/wallets/Jetton/index.ts | 65 ++++++----- .../renderer/lib/wallets/Jetton/index.ts | 109 +++++++++--------- 2 files changed, 92 insertions(+), 82 deletions(-) diff --git a/apps/recovery-relay/lib/wallets/Jetton/index.ts b/apps/recovery-relay/lib/wallets/Jetton/index.ts index 4e16d01..565626b 100644 --- a/apps/recovery-relay/lib/wallets/Jetton/index.ts +++ b/apps/recovery-relay/lib/wallets/Jetton/index.ts @@ -111,40 +111,45 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet { } public async prepare(): Promise { - // init the TonClient - this.init(); + try { + // init the TonClient + this.init(); - const jettonBalance = await this.getBalance(); - const contract = this.client!.open(this.tonWallet); - const tonBalance = await contract.getBalance(); + const jettonBalance = await this.getBalance(); + const contract = this.client!.open(this.tonWallet); + const tonBalance = await contract.getBalance(); - // fee for token tx is hardcoded to 0.1 TON - const feeRate = Number(toNano(0.1)); - await new Promise((resolve) => setTimeout(resolve, 2000)); + // fee for token tx is hardcoded to 0.1 TON + const feeRate = Number(toNano(0.1)); + await new Promise((resolve) => setTimeout(resolve, 2000)); - // get seqno of the wallet, set it as exrtaParams - const seqno = await this.getSeqno(); + // get seqno of the wallet, set it as exrtaParams + const seqno = await this.getSeqno(); - // get the contract address of the wallet - await new Promise((resolve) => setTimeout(resolve, 2000)); - const contractAddress = await this.getContractAddress(); - - // set extraParams - const extraParams = new Map(); - extraParams.set('seqno', seqno); - extraParams.set('contract-address', contractAddress?.toString({ bounceable: true, testOnly: false })); - extraParams.set('decimals', this.decimals); - - const preperedData = { - balance: jettonBalance, - memo: this.memo, - feeRate, - extraParams, - insufficientBalance: jettonBalance <= 0, - insufficientBalanceForTokenTransfer: tonBalance < feeRate, - } as AccountData; - - return preperedData; + // get the contract address of the wallet + await new Promise((resolve) => setTimeout(resolve, 2000)); + const contractAddress = await this.getContractAddress(); + + // set extraParams + const extraParams = new Map(); + extraParams.set('seqno', seqno.toString()); + extraParams.set('contract-address', contractAddress?.toString({ bounceable: true, testOnly: false })); + extraParams.set('decimals', this.decimals?.toString()); + + const preperedData = { + balance: jettonBalance, + memo: this.memo, + feeRate, + extraParams, + insufficientBalance: jettonBalance <= 0, + insufficientBalanceForTokenTransfer: tonBalance < feeRate, + } as AccountData; + + return preperedData; + } catch (e) { + this.relayLogger.error(`Jetton: Error preparing account data: ${e}`); + throw e; + } } private init() { diff --git a/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts index 71b22c9..c19ad1a 100644 --- a/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts +++ b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts @@ -9,64 +9,69 @@ export class Jetton extends BaseTon implements SigningWallet { } public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise { - // check for jetton extra params (contract address, seqno and decimals) - if (!extraParams?.get('contract-address') || !extraParams?.get('decimals') || !extraParams?.get('seqno')) { - throw new Error('Jetton: Missing jetton parameters'); - } + try { + // check for jetton extra params (contract address, seqno and decimals) + if (!extraParams?.get('contract-address') || !extraParams?.get('decimals') || !extraParams?.get('seqno')) { + throw new Error('Jetton: Missing jetton parameters'); + } + + const jettonTransferOpcode = 0x0f8a7ea5; + const decimals = Number(extraParams?.get('decimals')); + const normalizingFactor = 10 ** decimals; + const amountToWithdraw = amount * normalizingFactor; // amount is the wallet balance - const jettonTransferOpcode = 0x0f8a7ea5; - const decimals = extraParams?.get('decimals'); - const normalizingFactor = 10 ** decimals; - const amountToWithdraw = amount * normalizingFactor; // amount is the wallet balance + let internalMessageMemo = undefined; + // create the tx payload + const internalMessageBody = beginCell() + .storeUint(jettonTransferOpcode, 32) // opcode for jetton transfer + .storeUint(0, 64) // query id + .storeCoins(amountToWithdraw) // jetton balance + .storeAddress(Address.parse(to)) // tx destination + .storeAddress(Address.parse(this.address)) // excess fees sent back to native ton wallet + .storeBit(0); // no custom payload + if (memo) { + internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell(); + internalMessageBody + .storeCoins(1) // forward amount - if >0, will send notification message + .storeBit(1) // we store forwardPayload as a reference + .storeRef(internalMessageMemo) + .endCell(); + } else { + internalMessageBody + .storeCoins(0) // no memo added + .storeBit(0) + .endCell(); + } + const sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS; - let internalMessageMemo = undefined; - // create the tx payload - const internalMessageBody = beginCell() - .storeUint(jettonTransferOpcode, 32) // opcode for jetton transfer - .storeUint(0, 64) // query id - .storeCoins(amountToWithdraw) // jetton balance - .storeAddress(Address.parse(to)) // tx destination - .storeAddress(Address.parse(this.address)) // excess fees sent back to native ton wallet - .storeBit(0); // no custom payload - if (memo) { - internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell(); - internalMessageBody - .storeCoins(1) // forward amount - if >0, will send notification message - .storeBit(1) // we store forwardPayload as a reference - .storeRef(internalMessageMemo) + const contractAddress = extraParams?.get('contract-address'); + const internalMessage = beginCell() + .storeUint(0x18, 6) // bounceable tx + .storeAddress(Address.parse(contractAddress)) //wallet Jetton contract address + .storeCoins(BigInt(feeRate!)) + .storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // We store 1 that means we have body as a reference + .storeRef(internalMessageBody) .endCell(); - } else { - internalMessageBody - .storeCoins(0) // no memo added - .storeBit(0) + const toSign = beginCell() + .storeUint(698983191, 32) // Subwallet ID -> https://docs.ton.org/v3/guidelines/smart-contracts/howto/wallet#subwallet-ids + .storeUint(Math.floor(Date.now() / 1e3) + 600, 32) // Transaction expiration time, +600 = 10 minute + .storeUint(Number(extraParams?.get('seqno')), 32) // store seqno + .storeUint(0, 8) + .storeUint(sendMode, 8) + .storeRef(internalMessage) // store our internalMessage as a reference .endCell(); - } - const sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS; - const contractAddress = extraParams?.get('contract-address'); - const internalMessage = beginCell() - .storeUint(0x18, 6) // bounceable tx - .storeAddress(Address.parse(contractAddress)) //wallet Jetton contract address - .storeCoins(BigInt(feeRate!)) - .storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // We store 1 that means we have body as a reference - .storeRef(internalMessageBody) - .endCell(); - const toSign = beginCell() - .storeUint(698983191, 32) // Subwallet ID -> https://docs.ton.org/v3/guidelines/smart-contracts/howto/wallet#subwallet-ids - .storeUint(Math.floor(Date.now() / 1e3) + 600, 32) // Transaction expiration time, +600 = 10 minute - .storeUint(extraParams?.get('seqno'), 32) // store seqno - .storeUint(0, 8) - .storeUint(sendMode, 8) - .storeRef(internalMessage) // store our internalMessage as a reference - .endCell(); + const signMessage = toSign.toBoc().toString('base64'); + const signData = toSign.hash(); - const signMessage = toSign.toBoc().toString('base64'); - const signData = toSign.hash(); + const signature = Buffer.from(await this.sign(Uint8Array.from(signData))).toString('base64'); + const unsignedTx = Cell.fromBase64(signMessage).asBuilder(); - const signature = Buffer.from(await this.sign(Uint8Array.from(signData))).toString('base64'); - const unsignedTx = Cell.fromBase64(signMessage).asBuilder(); - - const body = beginCell().storeBuffer(Buffer.from(signature, 'base64')).storeBuilder(unsignedTx).endCell(); - return { tx: body.toBoc().toString('base64') }; + const body = beginCell().storeBuffer(Buffer.from(signature, 'base64')).storeBuilder(unsignedTx).endCell(); + return { tx: body.toBoc().toString('base64') }; + } catch (error) { + this.relayLogger.error(`Jettons: error with generating signed tx. error: ${error}`); + throw error; + } } }