Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@fireblocks/recovery-utility): 🐛 jetton withdrawals from seqno 0 wallets #134

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 35 additions & 30 deletions apps/recovery-relay/lib/wallets/Jetton/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,40 +111,45 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
}

public async prepare(): Promise<AccountData> {
// 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<string, any>();
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<string, any>();
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() {
Expand Down
109 changes: 57 additions & 52 deletions apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,69 @@ export class Jetton extends BaseTon implements SigningWallet {
}

public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise<TxPayload> {
// 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;
}
}
}